Skip to content

Phase 2: Stream responses to client via StreamingBody#585

Draft
aram356 wants to merge 5 commits intofeature/streaming-pipeline-phase1from
feature/streaming-pipeline-phase2
Draft

Phase 2: Stream responses to client via StreamingBody#585
aram356 wants to merge 5 commits intofeature/streaming-pipeline-phase1from
feature/streaming-pipeline-phase2

Conversation

@aram356
Copy link
Collaborator

@aram356 aram356 commented Mar 26, 2026

Summary

Stream HTTP responses directly to the client via Fastly's StreamingBody API when Next.js is disabled. This eliminates full-body buffering on the proxy path, reducing peak memory from ~4x response size to constant and improving TTFB.

Closes #573, closes #574, closes #575, closes #576.
Part of epic #563. Depends on Phase 1 (#583).

What changed

Entry point migration (main.rs):

  • Replaced #[fastly::main] with undecorated main() using Request::from_client()
  • route_request returns Option<Response>None when the streaming path already sent the response via stream_to_client()
  • All non-streaming routes use explicit send_to_client()

process_response_streaming now generic over W: Write (publisher.rs):

  • Changed from returning Body (internal Vec<u8>) to writing into &mut W
  • Enables passing StreamingBody directly as the output sink
  • Deduplicated PipelineConfig creation and content-encoding extraction

Streaming path via PublisherResponse enum (publisher.rs):

  • handle_publisher_request returns PublisherResponse::Stream or PublisherResponse::Buffered
  • Streaming gate: should_process && !request_host.is_empty() && (!is_html || !has_post_processors)
  • Synthetic ID / cookie headers set before body processing (body-independent)
  • Content-Length removed before streaming (chunked transfer)
  • stream_publisher_body() public API bridges core ↔ adapter
  • Error handling: pre-stream errors → send_to_client() with status; mid-stream → log and drop(streaming_body) (abort)

Files changed

File Lines What
main.rs +56 -15 Entry point migration, streaming dispatch
publisher.rs +185 -120 PublisherResponse enum, W: Write refactor, streaming gate

Task 10 (Chrome DevTools metrics) — deferred

Requires a running publisher origin to measure TTFB/TTLB. Local dev uses localhost:9090 which has no mock server. Metrics capture deferred to staging deployment. See #577.

Verification

  • cargo test --workspace — 754 passed, 0 failed
  • cargo clippy --workspace --all-targets --all-features -- -D warnings — clean
  • cargo fmt --all -- --check — clean
  • npx vitest run — 282 passed
  • cargo build --release --target wasm32-wasip1 — success

Test plan

aram356 added 5 commits March 26, 2026 14:29
Replace #[fastly::main] with an undecorated main() that calls
Request::from_client() and explicitly sends responses via
send_to_client(). This is required for Phase 2's stream_to_client()
support — #[fastly::main] auto-calls send_to_client() on the
returned Response, which is incompatible with streaming.

The program still compiles to wasm32-wasip1 and runs on Fastly
Compute — #[fastly::main] was just syntactic sugar.

Also simplifies route_request to return Response directly instead
of Result<Response, Error>, since it already converts all errors
to HTTP responses internally.
Change signature from returning Body (with internal Vec<u8>) to
writing into a generic &mut W: Write parameter. This enables
Task 8 to pass StreamingBody directly as the output sink.

The call site in handle_publisher_request passes &mut Vec<u8>
for now, preserving the buffered behavior until the streaming
path is wired up.
Split handle_publisher_request into streaming and buffered paths
based on the streaming gate:
- Streaming: 2xx + processable content + no HTML post-processors
- Buffered: post-processors registered (Next.js) or non-processable

Streaming path returns PublisherResponse::Stream with the origin
body and processing params. The adapter calls finalize_response()
to set all headers, then stream_to_client() to commit them, and
pipes the body through stream_publisher_body() into StreamingBody.

Synthetic ID/cookie headers are set before body processing (they
are body-independent), so they are included in the streamed headers.

Mid-stream errors log and drop the StreamingBody — client sees a
truncated response, standard proxy behavior.
- Replace streaming_body.finish().expect() with log::error on failure
  (expect panics in WASM, and headers are already committed anyway)
- Restore explanatory comments for cookie parsing, SSC capture,
  synthetic ID generation, and consent extraction ordering
Hoist the non-processable early return above the streaming gate so
content_encoding extraction happens once. The streaming gate condition
is also simplified since should_process and request_host are already
guaranteed at that point.
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