diff --git a/packages/aws-lambda/src/response.test.ts b/packages/aws-lambda/src/response.test.ts index b374b2c3..23ab5e2c 100644 --- a/packages/aws-lambda/src/response.test.ts +++ b/packages/aws-lambda/src/response.test.ts @@ -274,6 +274,37 @@ describe('sendStandardResponse', () => { await sendPromise }) + it.each([ + ['`from` throws', () => { + fromSpy.mockImplementationOnce(() => { + throw new Error('Cannot set content-type, too late.') + }) + }], + ['the first write throws', (responseStream: HttpResponseStream) => { + responseStream.write = () => { + throw new Error('write failed') + } + }], + ['the `awslambda` global is missing', () => { + vi.unstubAllGlobals() + }], + ])('rejects, destroys the response stream and the body when %s', async (_, setup) => { + const responseStream = createResponseStream() + setup(responseStream) + + const error = await sendStandardResponse(responseStream, { + status: 200, + headers: {}, + body: new Blob(['foo']), + }).catch(error => error) + + expect(error).toBeInstanceOf(Error) + expect(responseStream.errored).toBe(error) + + const [resBody] = toNodeHttpBodySpy.mock.results[0]!.value + expect((resBody as any).destroyed).toBe(true) + }) + describe('response stream closed before sending', () => { it('resolves and destroys the body', async () => { const responseStream = createResponseStream() diff --git a/packages/aws-lambda/src/response.ts b/packages/aws-lambda/src/response.ts index a832f60c..ab457427 100644 --- a/packages/aws-lambda/src/response.ts +++ b/packages/aws-lambda/src/response.ts @@ -41,41 +41,50 @@ export async function sendStandardResponse( return } - const [headers, setCookies] = toLambdaHeaders(resHeaders) + responseStream.once('error', reject) + responseStream.once('close', resolve) - // arms the metadata prelude (status, headers, cookies) and - // returns the stream the body should be written to - const res = awslambda.HttpResponseStream.from(responseStream, { - statusCode: standardResponse.status, - headers, - cookies: setCookies, - }) + try { + const [headers, setCookies] = toLambdaHeaders(resHeaders) - res.once('error', reject) - res.once('close', resolve) + // arms the metadata prelude (status, headers, cookies) and + // returns the stream the body should be written to + const res = awslambda.HttpResponseStream.from(responseStream, { + statusCode: standardResponse.status, + headers, + cookies: setCookies, + }) - // The runtime only sends the armed prelude ahead of the first `write` call: - // `end(chunk)` bypasses it and an empty body never writes, so trigger it now - res.write('') + // The runtime only sends the armed prelude ahead of the first `write` call: + // `end(chunk)` bypasses it and an empty body never writes, so trigger it now + res.write('') - if (resBody === undefined) { - // NOTE: Lambda functions don't allow passing undefined to `res.end` - res.end() - } - else if (typeof resBody === 'string') { - res.end(resBody) - } - else { - res.once('close', () => { - if (!resBody.closed) { - resBody.destroy(getNodeResponseError(res) ?? undefined) - } - }) + if (resBody === undefined) { + // NOTE: Lambda functions don't allow passing undefined to `res.end` + res.end() + } + else if (typeof resBody === 'string') { + res.end(resBody) + } + else { + res.once('close', () => { + if (!resBody.closed) { + resBody.destroy(getNodeResponseError(res) ?? undefined) + } + }) + + resBody.once('error', error => res.destroy(error)) - // WARNING: errors that occur here are silently ignored and not reported to the Promise - resBody.once('error', error => res.destroy(error)) + resBody.pipe(res) + } + } + catch (error) { + destroyNodeHttpBody(resBody, error, reject) - resBody.pipe(res) + // Destroy instead of leaving the response half-open: + // the metadata prelude may be partially applied + responseStream.destroy(error as any) + reject(error) } }) }