Skip to content

Commit ff403ee

Browse files
authored
fix(ext/fetch): use byte ReadableStream for Node Readable request bodies (#33432)
## Summary - When a `node:http` `IncomingMessage` (or any binary-mode `stream.Readable`) is passed as the body of a `Request`, build a `type: "bytes"` `ReadableStream` so consumers can acquire a BYOB reader (`getReader({ mode: "byob" })`). - Previously `extractBody` always wrapped async-iterable bodies with `ReadableStream.from(...)`, which produces a default (non-byte) stream. That broke code paths — including common HTTP proxying patterns — that call `request.body.getReader({ mode: "byob" })` on a body sourced from an `IncomingMessage`. - Matches undici's behavior in Node, where `stream.Readable` bodies go through `Readable.toWeb()` and yield a byte stream. Object-mode and encoded (string) readables still fall through to the existing `ReadableStream.from` path. Fixes #33392.
1 parent dc88ad5 commit ff403ee

2 files changed

Lines changed: 75 additions & 1 deletion

File tree

ext/fetch/22_body.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,40 @@ function extractBody(object) {
486486
throw new TypeError("ReadableStream is locked or disturbed");
487487
}
488488
} else if (object[webidl.AsyncIterable] === webidl.AsyncIterable) {
489-
stream = ReadableStream.from(object.open());
489+
// If the underlying body is a Node `Readable` running in binary mode
490+
// (e.g. `http.IncomingMessage`), build a byte `ReadableStream` so that
491+
// consumers can acquire a BYOB reader. This matches undici's behavior in
492+
// Node, where `stream.Readable` bodies go through `Readable.toWeb()`.
493+
const original = object.value;
494+
const readableState = (original !== null && typeof original === "object")
495+
? original._readableState
496+
: undefined;
497+
if (
498+
typeof readableState === "object" && readableState !== null &&
499+
!readableState.objectMode && !readableState.encoding
500+
) {
501+
const iter = object.open();
502+
stream = new ReadableStream({
503+
type: "bytes",
504+
async pull(controller) {
505+
// deno-lint-ignore prefer-primordials
506+
const res = await iter.next();
507+
if (res.done) {
508+
controller.close();
509+
} else {
510+
controller.enqueue(res.value);
511+
}
512+
},
513+
async cancel(reason) {
514+
if (iter.return !== undefined) {
515+
// deno-lint-ignore prefer-primordials
516+
await iter.return(reason);
517+
}
518+
},
519+
});
520+
} else {
521+
stream = ReadableStream.from(object.open());
522+
}
490523
}
491524
if (typeof source === "string") {
492525
// WARNING: this deviates from spec (expects length to be set)

tests/unit_node/http_test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2704,3 +2704,44 @@ Deno.test(
27042704
await promise;
27052705
},
27062706
);
2707+
2708+
// Regression test: a `node:http` IncomingMessage used as the body of a
2709+
// `Request` must produce a byte `ReadableStream`, so that
2710+
// `getReader({ mode: "byob" })` works.
2711+
// https://github.com/denoland/deno/issues/33392
2712+
Deno.test(
2713+
"[node/http] IncomingMessage as Request body supports BYOB reader",
2714+
async () => {
2715+
const { promise, resolve } = Promise.withResolvers<void>();
2716+
2717+
const server = http.createServer(async (req, res) => {
2718+
const request = new Request("http://localhost/", {
2719+
method: req.method,
2720+
headers: req.headers as HeadersInit,
2721+
body: req as unknown as BodyInit,
2722+
duplex: "half",
2723+
// deno-lint-ignore no-explicit-any
2724+
} as any);
2725+
2726+
const reader = request.body!.getReader({ mode: "byob" });
2727+
const buf = new Uint8Array(32);
2728+
const { value, done } = await reader.read(buf);
2729+
assertEquals(done, false);
2730+
assertEquals(new TextDecoder().decode(value), "hello world");
2731+
2732+
res.end("OK");
2733+
server.close(() => resolve());
2734+
});
2735+
2736+
server.listen(0, async () => {
2737+
const port = (server.address() as AddressInfo).port;
2738+
const res = await fetch(`http://localhost:${port}/`, {
2739+
method: "POST",
2740+
body: "hello world",
2741+
});
2742+
assertEquals(await res.text(), "OK");
2743+
});
2744+
2745+
await promise;
2746+
},
2747+
);

0 commit comments

Comments
 (0)