Repository navigation
ReadableStreamBYOBRequest.respondWithNewView() rejects valid views ("The underlying ArrayBuffer is not the correct length", "Too many bytes") #7692
Description
Which Cloudflare product(s) does this pertain to?
Workers Runtime (workerd).
What versions & operating system are you using?
workerd 1.20261011.1 and 1.20260730.1, compatibility date 2026-10-11 and
2026-07-30, no compatibility flags. macOS 26.7.1 (arm64). Same result with
pedantic_wpt, streams_no_default_auto_allocate_chunk_size, and at
compatibility date 2022-11-30.
Please provide a minimal reproduction
// respondWithNewView() with a view over the request's buffer, at the
// request's byteOffset, filling part of the request.
async function viewNotAtBufferStart() {
const stream = new ReadableStream({
type: "bytes",
pull(controller) {
const { buffer, byteOffset } = controller.byobRequest.view;
const view = new Uint8Array(buffer, byteOffset, 3);
view.set([1, 2, 3]);
controller.byobRequest.respondWithNewView(view);
},
});
const reader = stream.getReader({ mode: "byob" });
// The read's view starts at offset 4 of a 16-byte buffer.
const { value } = await reader.read(new Uint8Array(new ArrayBuffer(16), 4, 8));
return [...value];
}
async function afterPartialRespond() {
let pulls = 0;
const stream = new ReadableStream({
type: "bytes",
pull(controller) {
const request = controller.byobRequest;
if (++pulls === 1) {
// Below `min`, so the read stays pending and pull() runs again.
request.view.set([1, 2]);
request.respond(2);
return;
}
const { buffer, byteOffset } = request.view; // byteOffset is 2
const view = new Uint8Array(buffer, byteOffset, 3);
view.set([3, 4, 5]);
request.respondWithNewView(view);
},
});
const reader = stream.getReader({ mode: "byob" });
const { value } = await reader.read(new Uint8Array(8), { min: 4 });
return [...value];
}
const run = (f) => f().catch((e) => `${e.name}: ${e.message}`);
export default {
async fetch() {
return Response.json({
viewNotAtBufferStart: await run(viewNotAtBufferStart),
afterPartialRespond: await run(afterPartialRespond),
});
},
};Describe the bug
respondWithNewView() only accepts a view when the read's view covers its
whole ArrayBuffer and nothing has been filled yet. Otherwise it throws a
RangeError (and the stream errors):
- The read's view does not start at offset 0, or is shorter than its buffer:
"The underlying ArrayBuffer is not the correct length." - An earlier
respond()partly filled the request (aminread):
"Too many bytes [3] in response to a BYOB read request."
The same happens when the buffer has been transferred first, e.g. by a BYOB
read of another stream into byobRequest.view, which is the usual reason to
call respondWithNewView(). byobRequest.view being detached afterwards is
expected (Chrome and Node do the same); the rejection is the bug.
Looking at ByteQueue::ByobRequest::respondWithNewView() in
src/workerd/api/streams/queue.c++:
- It compares the new view's buffer length with
destView.size(), the byte
length of the read's view, instead of the byte length of the read's buffer. - It then replaces
req.pullInto.viewwith the caller's view and calls
respond(amount), which checksfilled + amountagainst that new view's
length (queue.c++L886-L887). Oncefilled > 0this always fails.
Per the spec, only the descriptor's buffer is replaced
(firstDescriptor.buffer = TransferArrayBuffer(view.buffer)); the byte offset
and byte length of the pull-into stay as they were.
Hit by Deno std's FormDataEncoderStream, which BYOB-reads a body into
byobRequest.view and then calls respondWithNewView(). Its test
"FormDataEncoderStream handles being cancelled midway through" fails with
"RangeError: Too many bytes [870] in response to a BYOB read request."
Expected
{"viewNotAtBufferStart":[1,2,3],"afterPartialRespond":[1,2,3,4,5]}, as in
Chromium 152 and Node 24.18.
ReadableByteStreamControllerRespondWithNewView
only throws a RangeError when:
firstDescriptor.byteOffset + firstDescriptor.bytesFilled ≠ view.byteOffsetfirstDescriptor.bytesFilled + view.byteLength > firstDescriptor.byteLengthfirstDescriptor.bufferByteLength ≠ view.buffer.byteLength
None of these hold in the repro.
Actual
{"viewNotAtBufferStart":"RangeError: The underlying ArrayBuffer is not the correct length.","afterPartialRespond":"RangeError: Too many bytes [3] in response to a BYOB read request."}🤖 Generated with Claude Code
Activity
jasnell commentedon Oct 11, 2026
Yep, known divergence. Requires a compat flag to fix. Will need to evaluate whether the new TS impl covers this