fix(res.send): preserve DataView byte ranges by nikhiljangid120 · Pull Request #7511 · expressjs/express · GitHub
Skip to content

fix(res.send): preserve DataView byte ranges - #7511

Open
nikhiljangid120 wants to merge 2 commits into
expressjs:masterfrom
nikhiljangid120:fix/send-dataview-bytes
Open

nikhiljangid120 wants to merge 2 commits into
expressjs:masterfrom
nikhiljangid120:fix/send-dataview-bytes

Conversation

@nikhiljangid120

@nikhiljangid120 nikhiljangid120 commented Oct 7, 2026 •

Copy link
Copy Markdown

Fixes #7510.

res.send(DataView) currently returns an empty body because Buffer.from(dataView) discards its contents. Convert only DataViews from their backing buffer, byte offset, and byte length. node:util.types.isDataView also recognizes cross-realm views.

This revisits #7433, which its author closed without merging. The fix preserves the existing Buffer, Uint8Array, and non-byte typed-array paths, without deciding the desired wire representation of multi-byte typed arrays. Bare ArrayBuffer support (#7362) is outside this change.

Verification on Node 24.14.1 / Linux:

  • Added 10 DataView test cases: full, sliced, empty, and cross-realm views with ETags enabled/disabled; explicit Content-Type; HEAD. Removed the Uint16Array element-truncation assertion after review so it does not establish that behavior as intended.
  • Eight new cases fail on the original implementation and pass with the fix.
  • npm run test-ci: 1,271 passing.
  • npm run lint: passed.
  • No dependencies, manifests, or lockfiles changed.

AI assistance was used for investigation, implementation, and local validation.

@ToniAdreal ToniAdreal left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Independently verified the root cause and the fix on Node v24.20.0:

  • Root cause confirmed. In res.send, a DataView lands in the ArrayBuffer.isView(chunk) arm, which only defaults the Content-Type to bin — the actual bufferization happens later, when the Content-Length/ETag path runs Buffer.from(chunk, encoding). Buffer.from has no DataView overload and returns an empty Buffer (Buffer.from(new DataView(buf)).length === 0, reproduced locally), so both Content-Length: 0 and the empty body are explained. The asymmetry with typed arrays is worth naming: Buffer.from(Uint16Array([0x0102, 0x0304])) performs an element-truncated copy (<02 04>), which is exactly why every non-DataView view never hit this bug — DataView is the one view type Buffer.from silently drops.
  • Fix shape is correct. Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength) goes through the ArrayBuffer overload and copies exactly the view's byte window. Using chunk.buffer.byteLength here would be wrong — it would leak bytes outside the window for sliced views — and the sliced-view regression case pins this.
  • The isDataView discriminant is load-bearing, not stylistic. instanceof DataView fails across realms (vm.runInNewContext('new DataView(...)') instanceof DataView === false, verified), while util.types.isDataView is realm-agnostic. The included cross-realm test case would fail with an instanceof check, so the import choice is doing real work.
  • Placement is right. Converting inside the isView arm, before the Content-Type default, keeps application/octet-stream for DataView bodies (asserted by the tests) and hands the later etagFn(chunk) a real Buffer, so weak-ETag generation matches Buffer-body behavior.
  • SAB edge closed too. A SharedArrayBuffer-backed DataView also produced an empty body on the old path (Buffer.from(sdv).length === 0), and converts without throwing on the fixed path (Buffer.from(sab, off, len) shares memory) — verified on Node 24.

The 11 new cases split cleanly: 8 are true regression cases (full/sliced/cross-realm views, the Content-Type override, HEAD Content-Length — all fail on the old code), and 3 are non-regression guards that pass on both versions, including the Uint16Array element-conversion behavior. LGTM.

@nikhiljangid120

Copy link
Copy Markdown
Author

@peralva

peralva commented Oct 15, 2026

Copy link
Copy Markdown

One question on scope: the test "should preserve element-based conversion for non-byte typed arrays" locks in res.send(new Uint16Array([0x0102, 0x0304])) sending 02 04, i.e. each element truncated to its low byte. That looks like the same root cause as the DataView bug (Buffer.from(view) copies element values instead of bytes) rather than intended behavior. Nobody sending a Uint16Array/Float32Array body would expect half of the data to be dropped.

Since ArrayBuffer.isView already matches DataViews and typed arrays from any realm, handling every non-Buffer view the same way would fix both cases and remove the need for isDataView:

} else if (ArrayBuffer.isView(chunk)) {
  if (!Buffer.isBuffer(chunk)) {
    chunk = Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
  }

This would change the output for multi-byte typed arrays, so it's a maintainer call whether that belongs in this PR or a separate one. I'd just suggest not adding a test that asserts the truncation.

@nikhiljangid120

Copy link
Copy Markdown
Author

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.

res.send(DataView) silently drops the view’s bytes

3 participants