feat: support Lambda response streaming end-to-end#179
Open
alexis779 wants to merge 1 commit into
Open
Conversation
f53d7e9 to
8c9a727
Compare
The non-direct invoke path used by the local Runtime Interface Emulator (the one SAM CLI talks to over /2015-03-31/functions/function/invocations) was buffering the runtime's /response body via io.ReadAll before writing a single slab back to the caller, and the rie.ResponseWriterProxy was storing that slab in a []byte. Both layers had to drain before the caller saw any byte, which made Server-Sent Events and other Lambda response-streaming workloads impossible to test locally. This change wires the real http.ResponseWriter all the way down: * rie.ResponseWriterProxy gains an Underlying http.ResponseWriter and becomes a streaming pass-through that copies staged headers/status on the first Write, then forwards every Write to the underlying writer and Flushes after each one. The body buffer is still kept for the pre-stream error paths in InvokeHandler. * rapidcore.Server.sendResponseUnsafe now detects streaming responses via additionalHeaders[Lambda-Runtime-Function-Response-Mode] (matched case-insensitively, since the runtime sends "streaming" and the interop constant is "Streaming") and uses a new streamingCopy helper that pipes the runtime's POST body through to the reply stream with a tiny 4KiB buffer and an explicit Flush after every Write. The buffered (legacy) branch is preserved and continues to enforce interop.MaxPayloadSize for non-streaming responses. * rie.InvokeHandler creates the proxy with the real ResponseWriter and skips the trailing WriteHeader/Write if streaming has already begun. Error code paths that would emit a synthetic body are gated on the new Started flag so we never try to overwrite an in-flight stream. End-to-end test: a Node.js handler using awslambda.streamifyResponse that emits one SSE frame per second for 10 seconds now reaches an EventSource client (and curl -N) one frame at a time, with the JSON HTTP-integration prelude correctly forwarded to the caller via the Content-Type and Lambda-Runtime-Function-Response-Mode headers. All existing unit tests under internal/lambda/... still pass. Co-authored-by: Cursor <cursoragent@cursor.com>
8c9a727 to
1e2a989
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR is the RIE half of the local Lambda response-streaming fix tracked by #175 (and the SAM CLI side aws/aws-sam-cli#6501).
Most of the design rationale, end-to-end test plan, and validation evidence live in the companion SAM CLI PR: aws/aws-sam-cli#9028. Please review the two together.
What this changes
The non-direct invoke path (the one SAM CLI hits over
/2015-03-31/functions/function/invocations) was:io.ReadAll-ing the runtime's/responsebody inrapidcore.Server.sendResponseUnsafebefore writing a single slab to the caller, and[]bytefield insiderie.ResponseWriterProxyuntilrie.InvokeHandlerdid one finalw.Write(invokeResp.Body).Both layers had to drain before the caller saw any byte, which made Server-Sent Events and any other Lambda response-streaming workload impossible to forward through RIE. This PR wires the real
http.ResponseWriterend-to-end:internal/lambda/rie/util.go—ResponseWriterProxynow optionally wraps an underlyinghttp.ResponseWriter. On the firstWriteit copies staged headers / status onto the underlying writer, then forwards everyWritestraight through and callsFlush()if the underlying supportshttp.Flusher. The legacy body buffer is kept for pre-stream error paths inInvokeHandler.internal/lambda/rapidcore/server.go—sendResponseUnsafedetects streaming viaadditionalHeaders["Lambda-Runtime-Function-Response-Mode"](matched case-insensitively, since the runtime sendsstreamingwhile the interop constant isStreaming) and uses a newstreamingCopyhelper. The helper uses a tiny 4 KiB buffer and an explicitFlushafter everyWriteso SSE frames a few dozen bytes long are not coalesced by Go's default 32 KiBio.Copybuffer. The buffered (legacy) branch is preserved and continues to enforceinterop.MaxPayloadSize.internal/lambda/rie/handlers.go— wires the realwinto the proxy and skips the trailingWriteHeader/Writeonce streaming has begun. The error code paths that re-emitinvokeResp.Body(ErrInitDoneFailed,ErrInvokeDoneFailed) are gated on the newStartedflag so we never try to overwrite an in-flight stream.Test plan
go test ./internal/lambda/...All existing tests pass. End-to-end validation (Node.js handler using
awslambda.streamifyResponse, the RIE binary built from this branch, SAM CLI'sstart-apiconsuming it, and a browserEventSource) is documented in aws/aws-sam-cli#9028 — the 10 SSE frames arrive at the browser at ~1 second intervals rather than all at once at the end, with the JSON HTTP-integration prelude correctly forwarded to the caller via theContent-TypeandLambda-Runtime-Function-Response-Modeheaders.Notes
internal/lambda/core/directinvokeis unchanged.Made with Cursor