Skip to content

ReadableStreamBYOBRequest.respondWithNewView() rejects valid views ("The underlying ArrayBuffer is not the correct length", "Too many bytes") #7692

Open

Description

@mikepage

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 (a min read):
    "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.view with the caller's view and calls
    respond(amount), which checks filled + amount against that new view's
    length (queue.c++ L886-L887). Once filled > 0 this 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.byteOffset
  • firstDescriptor.bytesFilled + view.byteLength > firstDescriptor.byteLength
  • firstDescriptor.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

  1. jasnell commented on Oct 11, 2026

    @jasnell
    Collaborator

    Yep, known divergence. Requires a compat flag to fix. Will need to evaluate whether the new TS impl covers this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions