Lambda-Runtime-Invocation-Id - #2508
Conversation
RuntimeApiHeaders exposes InvocationId (null when the header is absent, preserving backward compatibility). Surfaced on LambdaContext for the invoke loop to echo back on the response and error calls.
InternalRuntimeApiClient adds the Lambda-Runtime-Invocation-Id request header on /response and /error when present, added to the per-request message so concurrent invocations cannot echo each other's id. HTTP 410 Gone (Runtime.InvokeTimeout) maps to a new standalone RuntimeApiInvokeTimeoutException instead of the generic unexpected-status throw. Adds invocationId overloads to RuntimeApiClient and the public IRuntimeApiClient interface (following the SnapStart precedent of extending the runtime API client surface); existing overloads delegate with a null invocation id.
LambdaBootstrap threads the invocation id into the response and error POSTs and catches RuntimeApiInvokeTimeoutException before the general handler, logging via the internal logger and looping back to /next in both on-demand and multi-concurrency modes. The catch encloses both the response and error POST paths so a 410 on either does not crash the on-demand process. Updates the streaming test doubles to shadow the new invocationId overloads so LambdaBootstrap dispatches to their tracking logic.
Threads the invocation id through ResponseStreamFactory and ResponseStreamContext into RawStreamingHttpClient, which writes the Lambda-Runtime-Invocation-Id header into the raw HTTP header block before the terminating blank line and only when present.
Amazon.Lambda.RuntimeSupport, Minor increment.
The Release build treats the missing param tag as CS1573; add it so the build stays warning-clean.
There was a problem hiding this comment.
🟡 Not ready to approve
The PR introduces a breaking public interface change (new members on IRuntimeApiClient) and has a couple of fixable implementation issues (redundant computation + missing CR/LF sanitization when echoing invocation id into raw HTTP headers).
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Pull request overview
Adds support in Amazon.Lambda.RuntimeSupport for Lambda’s cross-wiring-on-invoke-id protection by propagating the per-invocation Lambda-Runtime-Invocation-Id header to /response, /error, and streaming responses, and by treating HTTP 410 (invoke timeout rejection) as non-fatal in the invoke loop.
Changes:
- Plumbs
Lambda-Runtime-Invocation-Idfrom/nextthrough context/bootstrap into buffered, error, and streaming response paths (echoed only when present). - Introduces 410→
RuntimeApiInvokeTimeoutExceptionhandling and updates the bootstrap loop to log-and-continue instead of crashing. - Updates/extends unit tests and test doubles to cover invocation-id propagation and 410 behavior.
File summaries
| File | Description |
|---|---|
| Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestStreamingRuntimeApiClient.cs | Updates streaming test client to accept/record invocation id. |
| Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestRuntimeApiClient.cs | Adds invocation-id-aware overloads and 410 simulation toggles for tests. |
| Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestMultiConcurrencyRuntimeApiClient.cs | Adds invocation-id overloads to multi-concurrency test client. |
| Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/NoOpInternalRuntimeApiClient.cs | Updates internal client stub signatures for invocation id. |
| Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/StreamingE2EWithMoq.cs | Captures invocation id in streaming E2E test clients. |
| Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/RuntimeApiClientTests.cs | Adds unit tests for invocation-id echo and 410 behavior. |
| Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamFactoryTests.cs | Ensures invocation id is passed into streaming start. |
| Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaResponseStreamingCoreTests.cs | Updates streaming client override signature for invocation id. |
| Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaContextTests.cs | Adds tests for reading invocation id from headers into context. |
| Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaBootstrapTests.cs | Adds tests for invocation-id echo and swallowing 410 in bootstrap. |
| Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaContext.cs | Exposes invocation id (internally) via context for bootstrap/streaming. |
| Libraries/src/Amazon.Lambda.RuntimeSupport/Client/RuntimeApiHeaders.cs | Parses/stores Lambda-Runtime-Invocation-Id from Runtime API headers. |
| Libraries/src/Amazon.Lambda.RuntimeSupport/Client/RuntimeApiClient.cs | Adds invocation-id-aware overloads for response/error/streaming. |
| Libraries/src/Amazon.Lambda.RuntimeSupport/Client/IRuntimeApiClient.cs | Adds new invocation-id-aware members to the public interface. |
| Libraries/src/Amazon.Lambda.RuntimeSupport/Client/InternalClientAdapted.cs | Echoes invocation id on /response and /error; throws on 410 Gone. |
| Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/ResponseStreaming/ResponseStreamFactory.cs | Passes invocation id into streaming start via context. |
| Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/ResponseStreaming/ResponseStreamContext.cs | Stores invocation id for streaming responses. |
| Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/ResponseStreaming/RawStreamingHttpClient.cs | Adds invocation id header to raw streaming HTTP request. |
| Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs | Plumbs invocation id through invoke loop and swallows 410 via log+continue. |
| .autover/changes/4133a18f-b6f9-4f24-886e-217756fbb670.json | Changelog entry for invocation-id support and 410 behavior. |
Review details
- Files reviewed: 20/20 changed files
- Comments generated: 5
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
| // Echo the per-invocation id back for cross-wiring protection. Only sent when the | ||
| // Runtime API provided it on /next. | ||
| if (!string.IsNullOrEmpty(invocationId)) | ||
| headers.Append($"{RuntimeApiHeaders.HeaderInvocationId}: {invocationId}\r\n"); | ||
| headers.Append("\r\n"); |
There was a problem hiding this comment.
should never happen
| // Echo the per-invocation id back for cross-wiring protection. Only sent when the | ||
| // Runtime API provided it on /next; added to this request message (never the shared | ||
| // HttpClient) so concurrent invocations cannot echo each other's id. | ||
| if (!string.IsNullOrEmpty(invocationId)) | ||
| request_.Headers.TryAddWithoutValidation(RuntimeApiHeaders.HeaderInvocationId, invocationId); | ||
|
|
There was a problem hiding this comment.
this should never happen
| // Echo the per-invocation id back for cross-wiring protection. Only sent when the | ||
| // Runtime API provided it on /next; added to this request message (never the shared | ||
| // HttpClient) so concurrent invocations cannot echo each other's id. | ||
| if (!string.IsNullOrEmpty(invocationId)) | ||
| request_.Headers.TryAddWithoutValidation(RuntimeApiHeaders.HeaderInvocationId, invocationId); | ||
|
|
There was a problem hiding this comment.
might be overkill should never happen
| /// <summary> | ||
| /// Report an invocation error as an asynchronous operation, echoing the invocation id for | ||
| /// cross-wiring protection. | ||
| /// </summary> | ||
| /// <param name="awsRequestId">The ID of the function request that caused the error.</param> | ||
| /// <param name="invocationId">The unique-per-invocation id to echo back to the Runtime API. When null, the header is not sent.</param> | ||
| /// <param name="exception">The exception to report.</param> | ||
| /// <param name="cancellationToken">The optional cancellation token to use.</param> | ||
| /// <returns>A Task representing the asynchronous operation.</returns> | ||
| /// <exception cref="RuntimeApiInvokeTimeoutException">The invocation timed out before the error was submitted (cross-wiring protection).</exception> | ||
| Task ReportInvocationErrorAsync(string awsRequestId, string invocationId, Exception exception, CancellationToken cancellationToken = default); | ||
|
|
There was a problem hiding this comment.
this interface should really have been internal. not worried about this as per norm
The non-invocationId overload computed ExceptionInfo.GetExceptionInfo but never used the result, then delegated to the invocationId overload which recomputes it. Drop the dead local; addresses PR review feedback.
…changes The Annotations source-generator tests compile the real InternalClientAdapted.cs and assert exact source coordinates in it. The cross-wiring changes shifted RuntimeApiSerializationContext and the JsonSerializerContext.Default usages down, so update the expected line numbers to match (columns unchanged). Verified the full Annotations suite passes 583/583 on net8.0 and net10.0.
Issue #, if available:
Description of changes:
What this adds
Support for Lambda's cross-wiring-on-invoke-id protection in the .NET runtime client (
Amazon.Lambda.RuntimeSupport).The Runtime API now sends a new per-invoke header,
Lambda-Runtime-Invocation-Id, onGET /runtime/invocation/next. This change makes the .NET runtime client:/next.POST /responseandPOST /error— only when it was present (backward compatible).ErrorRuntimeInvokeTimeoutrejection (HTTP 410 Gone), log via the internal logger and continue to the next invocation instead of crashing.Why it's needed (plain terms + example)
Lambda reuses an execution environment for many invocations, and two behaviors combine badly:
Because customers can supply their own request id (and upstream retries can reuse it), this race was possible:
The fix gives every invocation a unique nonce (
Lambda-Runtime-Invocation-Id) that is separate from the reusable request id. The runtime echoes it back; if it doesn't match the invocation the platform currently has active (or that invocation already timed out), the platform rejects the response with a 410 instead of misdelivering it. This PR is the .NET runtime's half of that contract.Backward compatible: if the header is absent from
/next(older platform), the runtime doesn't echo anything and behaves exactly as before.Testing
Unit tests — 8/8 passing
Amazon.Lambda.RuntimeSupport.UnitTests, covering: invocation-id echoed on/responseand/error; header omitted when absent (back-compat); 410 →RuntimeApiInvokeTimeoutException; and the bootstrap loop swallowing that exception (log + continue) on both the/responseand/errorpaths.Manual / integration testing
How this was tested. I used a custom RIE (one that implements the invoke timeout and 410 rejection) to run the real .NET runtime client, triggered invocations with
curl, and captured the client's socket traffic withstraceto confirm the header is actually on the wire. The scenarios below were run against the .NET 10 build of the client; the happy-path echo was also re-verified against the .NET 8 build, since the library targets both.In the captures below,
recvfrom= bytes the client received from the API (the/nextresponse) andsendto= bytes the client sent back (its/responseor/error). Note the echoedLambda-Runtime-Invocation-Idis a different value from the request id in the URL path — that's the whole point: the id is a separate per-invoke nonce, not the reusable request id.1. Normal invocation still works, and the id round-trips
A handler that just returns a value (input
"ping"→"pong"), HTTP 200. Confirms the new header handling didn't break the normal path, and that the client reads the id from/nextand echoes it on the response:2. The id is echoed on the error path too
A handler that throws, so the client reports the failure to
/errorinstead of/response. Confirms the echo happens on the error path as well:3. When a timed-out response is rejected (410), the client logs it and keeps going
This is the core behavior. I needed the invocation to time out while the handler was still running, so that the handler finishes and posts its response after the platform already gave up on it. To arrange that, I used a handler that deliberately runs for a fixed ~10 seconds regardless of the Lambda deadline:
Setting the function timeout to 2 seconds guarantees the timeout fires first. Observed sequence:
The client logged the rejection and went back to asking for the next invocation — the process stayed alive and did not crash. (Before this change, that 410 would have surfaced as an unhandled error.)
4. The id is echoed on the response-streaming path too
Response streaming sends the reply a different way — it builds the HTTP request by hand over a raw socket (chunked transfer), separately from the normal buffered path — so the echo had to be added there independently and is worth verifying on its own. Using a streaming handler (writes
"Hello, World!"), HTTP 200. Confirms the id is echoed on the streaming response:By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.