Skip to content

fix(node): encode string chunks back to bytes instead of corrupting them - #131

Merged
dinwwwh merged 1 commit into
mainfrom
claude/string-chunk-corruption-fix-76d7fa
Sep 29, 2026
Merged

dinwwwh merged 1 commit into
mainfrom
claude/string-chunk-corruption-fix-76d7fa

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Sep 29, 2026

Copy link
Copy Markdown
Member

Request bodies with string chunks are no longer corrupted. A Node stream yields strings instead of bytes when something upstream calls setEncoding on the request, or when it is an object-mode stream. toWebReadableStream passed those strings to new Uint8Array(value), which reads a string as a length: '123' became 123 zero bytes and most bodies came through empty. String chunks are now encoded back to bytes with the stream's own encoding.

Fixes

  • Octet-stream and event-stream bodies keep their real bytes instead of zeros or nothing.
  • JSON and URL-encoded bodies no longer throw a TypeError (and hang the request) on string chunks.
  • File bodies return the original bytes for non-utf8 encodings like base64, not the encoded text.
  • The Fastify adapter gets the fix through @standard-server/node. AWS Lambda was never affected, since it doesn't read from a Node stream.

API

  • New export readableChunkToBytes(stream, chunk), since utils.ts is re-exported from the package index.

Testing

  • New tests cover object-mode string streams, streams with utf8 / base64 / hex / latin1 encoding set (with a character split across chunks), and JSON and file requests with setEncoding called.
  • 8 of the 9 new tests fail without the fix. The one that passes is the utf8 file case, which already worked because File encodes strings as utf8.
  • All 1312 tests pass, and type-check and lint are clean.

A request stream yields string chunks when something upstream calls
`setEncoding` on it, or when it is an object-mode stream.
`toWebReadableStream` passed them to `new Uint8Array(value)`, which reads
a string as a length: '123' became 123 zero bytes and most bodies
vanished. The JSON and URL-encoded readers threw a TypeError on the same
input, and file bodies ignored the stream's encoding.

String chunks are now encoded back with the stream's own encoding
(utf8 by default), which recovers the original bytes for base64, hex
and latin1 as well.
@pkg-pr-new

pkg-pr-new Bot commented Sep 29, 2026

Copy link
Copy Markdown
@standard-server/aws-lambda

npm i https://pkg.pr.new/@standard-server/aws-lambda@131

@standard-server/core

npm i https://pkg.pr.new/@standard-server/core@131

@standard-server/fastify

npm i https://pkg.pr.new/@standard-server/fastify@131

@standard-server/fetch

npm i https://pkg.pr.new/@standard-server/fetch@131

@standard-server/node

npm i https://pkg.pr.new/@standard-server/node@131

@standard-server/peer

npm i https://pkg.pr.new/@standard-server/peer@131

@standard-server/shared

npm i https://pkg.pr.new/@standard-server/shared@131

commit: 042d69f

@codecov

codecov Bot commented Sep 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed

codspeed Bot commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 26 untouched benchmarks
⏩ 108 skipped benchmarks1


Comparing claude/string-chunk-corruption-fix-76d7fa (042d69f) with main (7d2294c)

Open in CodSpeed

Footnotes

  1. 108 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — a correct, well-tested fix. One adjacent body path (form-data) still bypasses the new helper.

Reviewed changes

  • New readableChunkToBytes helper — packages/node/src/utils.ts:64 re-encodes string chunks with Buffer.from(chunk, stream.readableEncoding ?? 'utf8') and passes byte chunks through.
  • Stream readers route through it — toWebReadableStream, _streamToString, and _streamToFile now convert chunks before use, fixing zero-filled/empty binary bodies and the TypeError that hung JSON/url-encoded requests under setEncoding or object mode.
  • Typing cleanup — _streamToFile chunks retyped to Uint8Array<ArrayBuffer>[]; the now-unused Buffer type import was dropped from body.ts.
  • Tests — object-mode string streams, utf8/base64/hex/latin1 encodings with bytes split across chunks, and JSON/file requests with setEncoding.

ℹ️ form-data bodies still bypass the new helper

_streamToFormData (packages/node/src/body.ts:156) hands the raw request Readable straight to new Response(stream, …).formData(), so its chunks never pass through readableChunkToBytes. With a lossless non-utf8 encoding set upstream (base64/hex/latin1) multipart parsing throws Failed to parse body as FormData. no boundary found in multipart body. This is pre-existing, not introduced here, and the realistic setEncoding('utf8') case is unrecoverable for binary parts anyway — but for the lossless encodings the same helper would restore the bytes, so it's the one stream-reader left inconsistent with the fix.

Technical details
# form-data path bypasses readableChunkToBytes

## Affected sites
- `packages/node/src/body.ts:156-164` — `_streamToFormData` constructs `new Response(stream, …)` from the raw Node `Readable`. undici treats each string chunk as already-decoded text rather than re-encoding via the stream's `readableEncoding`, unlike the other three readers touched by this PR.

## Evidence
- Reproduced on Node 24: a multipart request stream with `req.setEncoding('base64')` (also `hex`/`latin1`) throws `TypeError: Failed to parse body as FormData. Caused by: TypeError: no boundary found in multipart body`. The same body parses with the encoding unset or `utf8`.

## Required outcome
- The `form-data` branch should feed the parser the same bytes an un-encoded stream would, for encodings where that is recoverable.

## Suggested approach (optional)
- Pipe the source through the existing `toWebReadableStream(stream)` (which now applies `readableChunkToBytes`) before constructing the `Response`, or apply the helper per chunk in a small transform.

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

@dinwwwh
dinwwwh merged commit dab4327 into main Sep 29, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant