Skip to content

Run a test as a streaming function, so an event stream can be asserted - #331

Merged
ipjohnson merged 1 commit into
mainfrom
lambda-streaming-in-tests
Sep 10, 2026
Merged

ipjohnson merged 1 commit into
mainfrom
lambda-streaming-in-tests

Conversation

@ipjohnson

Copy link
Copy Markdown
Owner

From the 0.32 trial: C-02, reframed. The reported symptom is real and confirmed; the fix it implied is not the one worth building.

What the finding got right

Both causes are AWS's, confirmed by decompiling Amazon.Lambda.RuntimeSupport 2.2.0 rather than relayed.

public RawStreamingHttpClient(string hostAndPort) {
    string[] array = hostAndPort.Split(':');
    if (array.Length != 2) throw new ArgumentException(...);
    _host = array[0];
    _port = int.Parse(array[1], CultureInfo.InvariantCulture);
}

localhost:5050/Telemetry.Host splits into exactly two parts, so the guard passes and int.Parse throws. The trial's account is exact, and it is not the net11.0 arm's doing — the parsing is the same on every target.

The second cause rules out the obvious fix. The same client hardcodes its request line as POST /2018-06-01/runtime/invocation/{id}/response, with the function name nowhere in it. The Lambda Test Tool runs every function on one port and routes by a /{FunctionName} prefix, and lambda-test-tool start has no per-function-port flag — so shortening the endpoint fixes the parse and leaves the tool unable to route.

Neither is reachable from here, and neither is reported: RawStreamingHttpClient appears in no GitHub issue anywhere, searched across aws/aws-lambda-dotnet open and closed.

What it got wrong

"SSE cannot be exercised locally in the mode it deploys in." The locally is a platform-wide gap, not ours: RIE implements no response streaming for any language (#175 open since April with no comments, #179 unreviewed since May), and SAM local runs RIE underneath. Node had streaming years before .NET and cannot do this either.

The exercised was ours, and it did not need those two AWS pieces to talk. IResponseStreamFactory already exists as the seam — LambdaResponseStreamFactory is static with an internal setter, so nothing built on it can be driven from a test — and Hardened.Aws.Lambda.Testing simply had no wiring for it or for the response mode. An application could not run its own Lambda as a streaming function at all.

What this does

[LambdaWebTesting(ResponseMode = LambdaResponseMode.Stream)]
public class StreamedEventStreamTests {

    [HardenedTest]
    public async Task TheEventsArriveAsFramesOnTheResponseStream(ITestWebApp app) {
        var response = await app.Get("/orders/live");

        Assert.Equal("data: {\"id\":\"live-1\",\"quantity\":1}\n\n", Body(response));
    }
}

The mode registers StreamedResponseCapture over the runtime's factory and amends the configuration. A streamed invocation returns Stream.Null and writes to the response stream, so the host builds its answer from the prelude's status and headers and the bytes — the same Streamed path a deployed function takes.

It asks what happened rather than assuming the mode decided it: an adapter with no caller holding a connection stays buffered under the same mode, opens nothing, and the host reads its envelope as usual.

This closes a hole the repository already named. ServerSentEventManifestTests says the manifest and the buffered-mode warning are each covered and "neither says the two meet". Now something does.

The frames are not a discriminator

Three of the four new tests pass with the mode switched off. I know because I switched it off: 17 of 18 still passed. A buffered invocation writes identical SSE bytes — that is exactly what the buffered-mode warning is about, every event delivered at the end rather than as it happens — so a test asserting only on the body proves nothing about the mode.

[HardenedTest]
public async Task TheInvocationOpensALambdaResponseStream(
    ITestWebApp app, IResponseStreamFactory streams) {
    await app.Get("/orders/live");

    var capture = Assert.IsType<StreamedResponseCapture>(streams);

    Assert.True(capture.Opened, "the invocation wrote a proxy envelope rather than streaming");
    Assert.Equal(HttpStatusCode.OK, capture.Prelude!.StatusCode);
}

That is the one that fails, and aws/testing.md says so in bold, because anyone writing one of these would otherwise write the vacuous version.

Verification

  • dotnet test Hardened.slnx — 74 assemblies, 0 failures.
  • dotnet build Hardened.slnx --configuration Release -p:ContinuousIntegrationBuild=true — 0 warnings, 0 errors.
  • scripts/verify-templates.sh — every combination, no failures.
  • npm run build in docs/ — no dead links.
  • Public API re-approved: ResponseMode and StreamedResponseCapture. The streaming constructor on LambdaWebHost is internal, because a host built with a capture nothing resolves reads an empty response for every request.
  • aws/testing.md gains Testing a streaming function and states plainly why the Test Tool cannot do this; streaming.md points at it from the host table.

🤖 Generated with Claude Code

From the 0.32 trial: C-02, reframed. The reported symptom is that Lambda
streaming mode dies on the first request against the AWS Lambda Test Tool, and
both causes are AWS's own, confirmed by decompiling RuntimeSupport 2.2.0.
RawStreamingHttpClient reads AWS_LAMBDA_RUNTIME_API as host:port and parses the
rest as a port number, while the tool needs a function name in the path; and the
same client writes a request line with no function name in it, so shortening the
endpoint would leave the tool unable to route. Neither is reachable from here.

What the finding was really about is that an event stream could not be checked
in the mode it deploys in. That did not need those two to talk. The seam already
existed - IResponseStreamFactory, which exists because AWS's own factory is
static with an internal setter - and the testing library simply had no wiring
for it or for the response mode, so an application could not run its own Lambda
as a streaming function at all.

[LambdaWebTesting(ResponseMode = LambdaResponseMode.Stream)] registers
StreamedResponseCapture over the runtime's factory and amends the mode. A
streamed invocation returns Stream.Null and writes to the response stream, so
the host builds its answer from the prelude's status and headers and the bytes.

That closes a hole the repository already named: ServerSentEventManifestTests
says the manifest and the buffered-mode warning are each covered and "neither
says the two meet". Now something does.

The frames are not a discriminator. A buffered invocation writes identical
bytes, which is what the buffered-mode warning is about, so three of the four
new tests pass with the mode switched off - verified by switching it off. The
test that takes IResponseStreamFactory and asserts a stream was opened is the
one that fails, and the guide says so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ipjohnson
ipjohnson merged commit cac64ef into main Sep 10, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant