Repository navigation
Streaming Design #8
Replies: 3 comments
|
Section 1 (Streaming Responses) implemented in #14. Known limitations deferred to separate issues:
Section 2 (Request Body Streaming) remains for future work. |
Update:
|
|
Streaming responses have been implemented, though the design evolved significantly from what was sketched here. What was implemented:
Key differences from this design:
Not implemented:
|
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Two independent, composable features: streaming responses and streaming request bodies. Response streaming is the priority for initial implementation; request body streaming is designed here for future reference.
1. Streaming Responses
Any handler can send a chunked HTTP response. Stallion already has
start_chunked_response(),send_chunk(), andfinish_response()onResponder.Public API
New interface:
Context gains one method:
This calls
_responder.start_chunked_response()synchronously (we're inside_Connection's callback), sets_handled = true, and returns_Connection tagupcast toStreamSender tag. No wrapper actor, no extra message hop —_Connectionstructurally matchesStreamSenderby havingsend_chunkandfinishbehaviors that forward to the stored Responder.Internal Changes
Context:_conn: _Connection tag(passed asthisfrom_Connection)var _streaming: Bool = falsestart_streaming()— calls_responder.start_chunked_response(), sets_handledand_streaming, returns_connasStreamSender tagis_streaming(): Bool— read-only (box)_finish_streaming()— calls_responder.finish_response()for error cleanup (see "Error Handling for Partial Streaming")_Connection:var _streaming_responder: (Responder | None) = Nonesend_chunkandfinish(matchingStreamSender)_ChainRunnerreturns, checkctx.is_streaming()— if true, store the responder for later behavior turns_ChainRunner:ctx.is_streaming(), callctx._finish_streaming()to send the terminal chunk and prevent a hung connection (see "Error Handling for Partial Streaming")Usage
Why This Works with Capabilities
_Connectionis package-private, so users can't name its type — they only interact throughStreamSender tag. The only public behaviors on_Connectionaresend_chunkandfinish(everything from lori/stallion traits is_-prefixed orfun ref). No extra capabilities leak.2. Streaming Request Bodies (Future)
Routes opt in to receive body chunks incrementally instead of buffering the complete body.
Public API
New interfaces:
Both
HandlerandStreamingHandlerareinterface val— both can be primitives or classes. The handler is stateless and shareable. The difference is what the handler creates: aStreamingHandlercreates an actor (theBodyReceiver) to process body chunks over time, and returns it.Route registration — separate methods:
One
*_streamingvariant per HTTP method. Internally,_RouteDefinitiongains astreaming: Boolfield, and_RouteMatchstores(Handler | StreamingHandler).Internal Changes
For streaming routes, the handler is invoked in
on_request()(when headers arrive) instead ofon_request_complete():Non-streaming routes are completely unchanged —
on_request()is a no-op for them.Usage
StreamingHandler.apply()ReturnsBodyReceiver tag(Not Set via Context)The return type makes the contract explicit — you MUST provide a receiver. And if the handler errors, no receiver is set (error prevents the return), so chunks are safely dropped and 500 is sent. With a Context setter, the receiver could be set before an error, leaving state partially initialized.
3. Composition
Response streaming and request body streaming compose naturally. A
StreamingHandlercan callctx.start_streaming()to get aStreamSender, then return aBodyReceiverthat forwards processed data through the sender. Both features use the sameStreamSenderinterface.Response streaming is also available on non-streaming routes — any regular
Handlercan callctx.start_streaming().4. Design Decisions
Middleware Interaction Is Unchanged
before/afterphases run around the handler setup, not around the full streaming lifecycle. For streaming body routes,afterruns after the handler returns (receiver is set up but body hasn't arrived). This is consistent —afteris for request-level cleanup/logging, not streaming lifecycle management.HTTP/1.0 Limitation
Stallion silently rejects chunked encoding for HTTP/1.0.
start_streaming()always returns aStreamSender, but chunks are silently dropped for HTTP/1.0 clients. This is documented but not expressed in the type (HTTP/1.0 is vanishingly rare).Flow Control Is Deferred
Stallion has
on_chunk_sent(token)andon_throttled()/on_unthrottled()callbacks. Exposing these throughStreamSenderis a natural future extension but not included initially.Error Handling for Partial Streaming
If a handler calls
start_streaming()and then errors, the chunked response has already started (status line and headers are on the wire) but nobody will callfinish(). Since_handledistrue,_ChainRunnerwould normally skip the 500 — leaving the connection hung with an unterminated chunked response.To prevent this,
_ChainRunnertracks whether the handler errored. If it errored AND the context is in streaming mode, it finishes the stream (sends the terminal chunk via a package-privateContext._finish_streaming()method that calls_responder.finish_response()). This applies to both response-only streaming and streaming body routes (where the handler errors before returning aBodyReceiver).Streaming Body Responses Always Use Chunked Transfer Encoding
If a streaming body handler wants to send a deferred response after body processing, it starts streaming and sends a single chunk + finish. This is valid HTTP/1.1 and avoids needing a separate deferred-response mechanism. Content-Length responses require the non-streaming path.
Separate
*_streaming()Methods vs. ParameterSeparate methods take
StreamingHandlerinstead ofHandler, making a type mismatch a compile error. A parameter approach would need a runtime check.5. What Changes, What Doesn't
HandlerMiddlewareContextstart_streaming(),is_streaming(),_finish_streaming(),_connfield,_streamingflagApplication*_streaming()route methods (request body streaming only)_Connectionon_request()override (request body streaming only)_ChainRunner(Handler | StreamingHandler)union (request body streaming only)_RouteDefinitionstreamingflag (request body streaming only)_RouteMatch(Handler | StreamingHandler)(request body streaming only)_RouterStreamSenderStreamingHandlerBodyReceiverAll reactions