Skip to content

feat(procedures): add streaming HTTP response support#4692

Open
philtrem wants to merge 4 commits intoclockworklabs:masterfrom
philtrem:feat/http-streaming
Open

feat(procedures): add streaming HTTP response support#4692
philtrem wants to merge 4 commits intoclockworklabs:masterfrom
philtrem:feat/http-streaming

Conversation

@philtrem
Copy link

@philtrem philtrem commented Mar 23, 2026

Description of Changes

Adds fetchStreaming() to the TypeScript procedure HTTP client, returning a StreamingResponse that yields body chunks via synchronous iteration. This enables procedures to process large HTTP responses without buffering the entire body in memory.

Depends on #4691 (headers fix is the first commit in this branch).

Architecture:

  • Background tokio task reads chunks from the HTTP response body into a bounded mpsc channel (capacity 8)
  • The V8 thread reads synchronously one chunk at a time via procedure_http_stream_next
  • Extracts shared prepare_http_request() to reuse IP filtering, DNS filtering, timeout clamping, and redirect policy between fetch() and fetchStreaming()
  • Transaction guard on stream_next prevents blocking inside withTx
  • Resource cleanup via FinalizationRegistry backup + explicit Symbol.dispose() + HttpStreamState::drop aborts background task
  • Follows the established ResourceSlab / handle pattern used by row iterators

API and ABI breaking changes

None. Adds new fetchStreaming() method to HttpClient and 3 new V8 syscalls (procedure_http_stream_open, procedure_http_stream_next, procedure_http_stream_close). Existing fetch() API is unchanged.

Expected complexity level and risk

3 — Introduces a new streaming code path through the syscall layer, instance environment, and TypeScript bindings. The main interaction concern is blocking the V8 thread per chunk, which stalls all other reducers/procedures for the module instance while waiting. This is documented and is a necessary trade-off until async procedures are supported. The Rust-side changes reuse existing infrastructure (ResourceSlab, prepare_http_request, metrics) to minimize new surface area.

Testing

  • Unit test: BSATN header round-trip
  • Unit test: http_request blocked during transaction
  • Unit test: http_stream_open blocked during transaction
  • Unit test: HttpStreamState::drop aborts background task
  • Smoketest: basic streaming read (chunked encoding, chunks concatenated correctly)
  • Smoketest: stream.next() inside transaction throws WouldBlockTransaction
  • Smoketest: response headers preserved through streaming path
  • Reviewer: verify streaming with a slow/large response source if desired

philtrem and others added 3 commits March 21, 2026 01:43
…urning empty Headers

fetch() was deserializing the HttpResponse (including headers) from BSATN
but then discarding them, always returning `new Headers()`. This made it
impossible for procedures to inspect response metadata like Content-Type
or retry hints.

Add a `deserializeHeaders()` helper that converts the BSATN-decoded
HttpHeaders entries into a web-standard Headers object, and use it in
fetch().
…modules

Add fetchStreaming() to the procedure HTTP client, allowing TypeScript
modules to consume HTTP response bodies chunk by chunk without buffering
the entire response in memory.

Syscall layer:
- procedure_http_stream_open: initiates the request and returns a handle
  plus BSATN-encoded response metadata (status, headers)
- procedure_http_stream_next: blocks until the next chunk arrives or the
  stream ends; guarded by in_tx() to prevent holding a mutable
  transaction open across network waits
- procedure_http_stream_close: closes the handle and aborts the
  background reader task

Runtime:
- HttpStreamState stores the mpsc receiver and a tokio AbortHandle;
  Drop impl aborts the background reader immediately rather than
  waiting for the next frame or timeout
- Streaming response size is metered via the existing
  procedure_http_response_size_bytes counter (header size at open,
  per-chunk body size at read)

TypeScript bindings:
- StreamHandle class with FinalizationRegistry safety net
- StreamingResponse interface with Symbol.iterator / Symbol.dispose
- fetchStreaming() uses deserializeHeaders() from the preceding commit
  to expose response headers

Tests:
- Unit: WouldBlockTransaction guards on http_request and http_stream_open
- Unit: HttpStreamState::drop aborts the background task
- Smoketest: end-to-end streaming read, tx-blocked iteration, and header
  preservation against a local chunked HTTP server
Ensures the stream handle is registered with FinalizationRegistry
before any code that could throw, preventing a handle leak if
HttpResponse.deserialize() ever fails.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@CLAassistant
Copy link

CLAassistant commented Mar 23, 2026

CLA assistant check
All committers have signed the CLA.

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.

2 participants