Skip to content
31 changes: 31 additions & 0 deletions packages/aws-lambda/src/response.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
67 changes: 38 additions & 29 deletions packages/aws-lambda/src/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
Loading