Skip to content

Normalize decompressed bytes to a base Uint8Array (Node buffer breaks RSC consumers) #1771

Description

@aaronmhargrove

Is your feature request related to a problem? Please describe.

Decompressed response bytes from compressed payloads come back as a Node Buffer instances rather than the base Uint8Array type. That itself isn't an explicit issue, the Buffer does subclass the Uint8Array so is technically correct.

The issue we're having is that when that gets into react-server-land, react server components break when passing data through the server-client boundary because the Buffer has a toJSON method, which react (seemingly intentionally) does not honor as binary data. acceptCompression defaults to [compressionGzip, compressionBrotli], so any response the server compresses hits this.

For us, this surfaces in Any.value and $unknown[].data values since those end up inheriting the Buffer subclass. React then applies Buffer.toJSON(), spitting out { type: 'Buffer', data: [...] } which doesn't error but breaks client side decodes downstream.

Describe the solution you'd like

The fix would be to have connect-node's asUint8ArrayArrayBuffer (packages/connect-node/src/compression.ts) check for subclasses of Uint8Array and normalize on that type to ensure its compatible for structured-clone style consumers. This could be done with a zero-copy into a Uint8Array so it would be effectively free and still conforms to the expected type.

function asUint8ArrayArrayBuffer(
  bytes: Promise<Uint8Array>,
): Promise<Uint8Array<ArrayBuffer>> {
  return bytes.then((b) => {
    if (b.buffer instanceof ArrayBuffer) {
      // Node hands back Node Buffers. Re-view so consumers get a base Uint8Array
      return b.constructor === Uint8Array
        ? (b as Uint8Array<ArrayBuffer>)
        : new Uint8Array(b.buffer, b.byteOffset, b.byteLength);
    }
    return new Uint8Array(b);
  });
}

To be clear, I don't think anything is broken here, asUint8ArrayBuffer is doing its stated job and we found a workaround. It just has a bit of a server <-> client footgun that consumers can't see from the Uint8Array type, and may result in downstream decode failures far from the cause.

Describe alternatives you've considered

  1. Normalize in app code - What we're doing now and it seems to work. We've added a wrapper around all of our connect-node services that recursively check every response payload to transform node buffers into Uint8Array values.
  2. Change in React - The react team seems to have considered this and chose to deliberately not preserve the binary data as a Uint8Array, and warn to not pass node buffers instead.
  3. Leave the behavior, document it — a note that decompressed bytes may be Buffers and should be normalized before crossing a structured-clone boundary, such as with React server components

Activity

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

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions