js: read XHR response bodies as bytes, not through text() - #755
Open
alexskinner wants to merge 1 commit into
Open
js: read XHR response bodies as bytes, not through text()#755alexskinner wants to merge 1 commit into
alexskinner wants to merge 1 commit into
Conversation
XMLHttpRequest is implemented over fetch and read every response with
resp.text() before choosing a responseType, so the binary types were built
from a string:
case 'arraybuffer': xhr.response = new TextEncoder().encode(text).buffer;
case 'blob': xhr.response = new Blob([text]);
text() -> TextEncoder().encode() is not a round-trip for bytes that are not
valid UTF-8. A lenient decode treats any high byte as a lead byte, so 0x82 0x83
becomes U+0083 and re-encodes as c2 83; every byte >= 0x80 is rewritten and the
length changes with the content. fetch() was unaffected because it never took
that detour.
Read the body once as an ArrayBuffer and decode to text only for the text-ish
response types. Not decoding a multi-megabyte binary body also avoids building
a string nothing reads; responseText is not defined for the binary types, so it
is left empty rather than throwing.
A 256-byte file of every byte value now round-trips identically on both paths,
matching Chrome. A 4,196,020-byte asset that arrived as 4,118,955 bytes with a
corrupted header is now byte-exact, and an Emscripten application that aborted
while parsing it starts and runs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #754.
XMLHttpRequestis implemented overfetchand read every response withresp.text()before choosing aresponseType, so the binary types were rebuiltfrom a string:
text()→TextEncoder().encode()is not a round-trip for bytes that are notvalid UTF-8. A lenient decode treats any high byte as a lead byte, so
82 83becomes U+0083 and re-encodes as
c2 83— every byte ≥0x80is rewritten andthe length changes with the content.
fetch()was unaffected because it nevertook that detour, which is why this hid: ASCII bodies, and therefore all JSON
and text over XHR, are untouched.
The change
Read the body once as an
ArrayBufferand decode to text only for the text-ishresponse types.
blobhad the same bug and is fixed with it. Not decoding the body for thebinary types also avoids building a multi-megabyte string that nothing reads;
responseTextis not defined for those types, so it is left''rather thanthrowing — the conservative option, but say if you would rather it threw.
Test
crates/obscura/tests/xhr_binary_response.rsserves a 256-byte fixture holdingevery byte value and asserts on the bytes, not just the length — a same-length
corruption would otherwise pass. Confirmed to fail without the fix:
Before / after
On a real 4,196,020-byte asset, XHR returned 4,118,955 bytes with a corrupted
header; it is now byte-exact. This matters beyond the obvious, because
Emscripten's generated loader fetches
.wasmand data files withXMLHttpRequest+responseType='arraybuffer'rather thanfetch()— so WASMapplications were loading corrupted assets and failing far from the cause. One
that previously aborted on a nonsense allocation derived from a mangled header
now starts and runs, rendering to a 2D canvas and responding to
Input.dispatchKeyEvent.Related to #716, which covers binary request bodies and the CDP fulfill
path. This is the response side of XHR and a separate code path, though likely
the same underlying habit of reaching for
StringwhereVec<u8>belongs.