Deno.serve and Fetch API: Request Body Stream Consumption Behavior
0 reputation · 10 Nov 2021, 17:29 UTC
0 reputation · 10 Nov 2021, 17:29 UTC
The Deno.serve() API utilizes the Web Standard Fetch API, allowing handlers to process incoming Request objects. For large payloads, Deno leverages ReadableStream to maintain memory efficiency by avoiding the buffering of the entire request body into memory.
When integrating custom middleware or multiple processing layers, there is a design uncertainty regarding the consumption of the request body stream. Since a ReadableStream is typically locked once a reader is acquired, passing the Request object through several asynchronous validation or logging layers may lead to stream exhaustion before the final business logic can access the data.
Given the current implementation of the Fetch API in Deno:
Request object provide a native mechanism to clone the body stream without duplicating the underlying memory buffer?35775 reputation · 10 Nov 2021, 20:19 UTC
The Request object in Deno.serve follows the Fetch API specification: its body is a ReadableStream that can be read only once. After any consumption (e.g., await request.text(), request.json(), or obtaining a reader), the stream is locked and further reads return empty data or throw a TypeError.
Yes. Calling request.clone() creates a new Request whose body is a clone of the original stream. The underlying bytes are not duplicated until each clone is actually read; each clone can be consumed independently exactly once. This satisfies the requirement of “no native buffering” while allowing multiple readers.
const handler1Req = request.clone();
const handler2Req = request.clone();
// …additional clones as required
await handler1Req.text(), handler2Req.json(), or handler1Req.body.getReader())..text() after obtaining a reader on that clone).If you need to split a single stream into multiple concurrent readers without cloning the request (e.g., to avoid creating several full copies), you can use a TransformStream with tee():
const { readable, writable } = new TransformStream();
request.body.pipeTo(writable);
const [branchA, branchB] = readable.tee();
// now read branchA and branchB independently
This approach keeps the data in memory only as it flows through the branches.
If your middleware needs to access the original Request object after all body consumption (for example, to inspect headers or to forward the request elsewhere), cloning alone will not suffice because the original body will be exhausted. In that case you must buffer the body (e.g., const text = await request.text();) and work with the buffered string/bytes, or redesign the flow so that body consumption happens last. Let me know whether you need to reuse the original request after the body has been read, and I can adjust the recommendation.
Use comments to ask for clarification. Post a solution as an answer.
35,775 reputation · 11 Nov 2021, 02:08 UTC
When routing a single Request through multiple middleware layers in Deno.serve, the body stream can only be consumed once without cloning. The Fetch API specification, as implemented in Deno, does not buffer the payload for repeated access; after the first read, the stream locks and subsequent attempts return empty data. To safely expose the body to several handlers, extract a clone early—request.clone() yields a new Request whose body is an independent tee of the original stream. Each clone can then be consumed separately via .text(), .json(), or a ReadableStream reader, without interfering with the others. For cases where cloning upfront is undesirable, request.body.tee() splits the stream into two branches that buffer chunks until both readers advance, which can raise peak memory usage for large payloads. Verifying the pattern involves cloning before any consumption and confirming each branch reads independently.