Skip to content

Web streams respondWithNewView() errors when view.byteOffset != 0 #42851

Closed
@Macil

Description

@Macil

Version

v18.0.0

Platform

No response

Subsystem

No response

What steps will reproduce the bug?

If you call ReadableStreamBYOBRequest.respondWithNewView() with a Uint8Array that has a byteOffset greater than 0, then you get an error, even when the regular required conditions are met (the view's byteOffset equals the byobRequest.view's byteOffset, it refers to the same buffer, etc). This makes it impossible to use if the byobRequest.view's byteOffset is greater than 0.

async function main() {
  const readable = new ReadableStream({
    type: "bytes",
    autoAllocateChunkSize: 128,
    pull(controller) {
      const view = controller.byobRequest.view;
      const dest = new Uint8Array(
        view.buffer,
        view.byteOffset,
        view.byteLength
      );
      for (let i = 0; i < dest.length; i++) {
        dest[i] = i;
      }
      controller.byobRequest.respondWithNewView(dest);
      // Code works fine if use .respond() instead of .respondWithNewView():
      // controller.byobRequest.respond(dest.length);
    },
  });

  const reader = readable.getReader({ mode: "byob" });
  
  const buffer = new ArrayBuffer(10);
  const view = new Uint8Array(
    buffer,
    5,
    3
  );
  const read = await reader.read(view);
  console.log("read", read);
  console.log("full buffer", new Uint8Array(read.value.buffer));
}

main();

How often does it reproduce? Is there a required condition?

This bug seems to be caused by line 2522 of readablestream.js:

  if (bytesFilled + viewByteOffset > byteLength)
    throw new ERR_INVALID_ARG_VALUE.RangeError('view', view);

byteLength is the length of the view, and bytesFilled is equal to the length of the view in this example (and any other case a read fills up the provided view), so an error would happen whenever the view's byte offset is greater than zero.

I assume byteLength is supposed to be viewBufferByteLength instead here, because it doesn't make sense to compare a calculation based on the view's byte offset to the view's length, but it would make sense to compare that calculation to the underlying buffer's length.

What is the expected behavior?

Here is the expected output that you get when run in Deno or Chrome:

read { value: Uint8Array(3) [ 0, 1, 2 ], done: false }
full buffer Uint8Array(10) [
  0, 0, 0, 0, 0,
  0, 1, 2, 0, 0
]

What do you see instead?

Here is the actual output that you get from Node:

node:internal/errors:465
    ErrorCaptureStackTrace(err);
    ^

RangeError [ERR_INVALID_ARG_VALUE]: The argument 'view' is invalid. Received Uint8Array(3) [ 0, 1, 2 ]
    at new NodeError (node:internal/errors:372:5)
    at readableByteStreamControllerRespondWithNewView (node:internal/webstreams/readablestream:2523:11)
    at ReadableStreamBYOBRequest.respondWithNewView (node:internal/webstreams/readablestream:682:5)
    at Object.pull (/Users/macil/Coding/node-webstream-test/test-simple.js:15:30)
    at ensureIsPromise (node:internal/webstreams/util:172:19)
    at readableByteStreamControllerCallPullIfNeeded (node:internal/webstreams/readablestream:2548:5)
    at node:internal/webstreams/readablestream:2666:7 {
  code: 'ERR_INVALID_ARG_VALUE'
}

Node.js v18.0.0

Additional information

I ran into this issue when using code similar to the readInto() example inside the Streams standard for filling a buffer using a bring-your-own-buffer mode reader of a stream that made use of respondWithNewView().

This issue can be worked around in some cases by using respond() instead of respondWithNewView(), but this can only be done when the byobRequest.view has not been transferred, which is easily possible if a ReadableStream is constructed that is wrapping another one.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions