Repository navigation
fix(aws-lambda): destroy the body when response stream setup throws - #126
Conversation
… throws
`awslambda.HttpResponseStream.from()` and the prelude-triggering `write('')`
can throw synchronously (e.g. the runtime's "Cannot set content-type, too
late." on an already-written stream, or a missing `awslambda` global). The
promise rejected but the body was never destroyed, so event iterators never
ran their `finally`, the keep-alive interval kept firing, and ReadableStream
bodies were never cancelled.
Mirror the node adapter: on a synchronous throw, destroy the body, destroy
the response stream instead of leaving it half-open, and reject.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AVoginFKKdE3QrCKUARJ98
Attach the `error`/`close` listeners to the response stream before the try, like the node adapter, so the catch no longer needs its own `error` listener to cover a throwing `from`. Collapse the three throw-path tests into one `it.each`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AVoginFKKdE3QrCKUARJ98
Fold the stream branch's separate `close` listener into the one attached before the try, as the fastify adapter now does. The body is released in one place for every path, one fewer listener per streaming response, and settle/cleanup no longer listen on differently named aliases of the same stream. On the catch path the body is already destroyed, so the listener's destroy is a no-op. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AVoginFKKdE3QrCKUARJ98
Replace the inline body teardown in the catch with the shared helper, as the node and fastify adapters now do. Drop the "errors are silently ignored" warning copied from the node adapter: here `res.destroy(error)` emits `error` on the response stream, which rejects the promise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AVoginFKKdE3QrCKUARJ98
@standard-server/aws-lambda
@standard-server/core
@standard-server/fastify
@standard-server/fetch
@standard-server/node
@standard-server/peer
@standard-server/shared
commit: |
Merging this PR will not alter performance
Comparing Footnotes
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Response setup wrapped in
try/catch:sendStandardResponsenow catches synchronous failures fromtoLambdaHeaders,HttpResponseStream.from(), the preluderes.write(''), andend(), tears the body down viadestroyNodeHttpBody, destroys the response stream, and rejects with the thrown error. - Listeners attached up front: the
error/closehandlers moved ontoresponseStreamahead of setup, so a throwingfrom()still has an error listener, and the singleclosehandler now performs the guarded body teardown for every path instead of only the stream branch. - New failure tests: three
it.eachcases (fromthrows, firstwritethrows,awslambdaglobal missing) assert rejection,responseStream.errored === error, and a destroyed body.
I also confirmed the new cases are discriminating: reverting response.ts to b5b4035 while keeping the new tests makes all three fail with responseStream.errored still null, so they genuinely pin the cleanup. Once the tree is restored, the package tests, type:check, and lint are all clean.
DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏
… listener Restore the original stream-branch `close` listener for destroying the body and keep the pre-try `close` listener to `resolve` only, matching main and the node adapter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AVoginFKKdE3QrCKUARJ98
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
The only change since the prior pullfrog review (ed8585d) is one behavior-preserving refactor (fa4baf5):
- Body teardown back in the stream-branch close listener:
responseStream.once('close', …)now onlyresolves, while the guardedresBody.destroy(getNodeResponseError(res) ?? undefined)moved into the object-bodyelsebranch. This restores the success path to its pre-PR shape and mirrors the node adapter's structure. - Setup-error cleanup unchanged: the
try/catchstill callsdestroyNodeHttpBody(resBody, error, reject), destroys the response stream, and rejects — so the fix the PR exists for is intact.
Since the runtime's from() returns the same stream it is handed, res === responseStream, making the relocation behavior-equivalent for every path. pnpm vitest run packages/aws-lambda/src/response.test.ts passes 13/13.
DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

The aws-lambda
sendStandardResponsenow releases the response body when setting up the response stream throws synchronously. Before,awslambda.HttpResponseStream.from()or the prelude-triggeringres.write('')could throw. The promise rejected, but the bodytoNodeHttpBodyhad already built was never destroyed. An event-stream generator never ran itsfinallyand its keep-alive interval kept firing; aReadableStreambody was never cancelled. On a warm Lambda container these leaks carried over into later invocations.The setup now runs inside a
try, like the node adapter. Thecatchdestroys the body withdestroyNodeHttpBody, destroys the response stream instead of leaving it half-open, and rejects with the original error.Behaviour
Cannot set content-type, too late.when the stream was already written to (canWriteToNodeResponsecan't detect that on the Lambda stream), a missingawslambdaglobal, or a throwing first write.error/closelisteners that settle the promise are now attached toresponseStreambefore thetry. The Lambda runtime'sfrom()returns the same stream it is given, so this is the same object as before. It also means a throwingfrom()can't leave theerrorevent fromdestroyunhandled.closelistener that destroys the body.WARNING: errors that occur here are silently ignoredcomment that was copied from the node adapter. It isn't true here:res.destroy(error)emitserroron the response stream, which rejects the promise, as the existing body-error test checks.Testing
it.eachcovering three cases:fromthrows, the first write throws, andawslambdais missing. Each asserts the rejection,responseStream.errored, and that the body was destroyed. All three fail againstmain.vitest run packages/aws-lambda(100 passed),eslint packages/aws-lambda, andtsc -bare clean.🤖 Generated with Claude Code
https://claude.ai/code/session_01AVoginFKKdE3QrCKUARJ98