Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions crates/perry-stdlib/src/fetch/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,20 @@ pub(crate) unsafe fn body_value_buffer_bytes(value: f64) -> Option<Vec<u8>> {
return None;
}
};
body_addr_buffer_bytes(addr)
}

/// Registry-probe core shared by `body_value_buffer_bytes` (which first decodes
/// a NaN-boxed body *value* to an address) and `js_request_new` (whose body
/// argument codegen already decoded to a raw heap address via
/// `js_get_string_pointer_unified`). Returns a copy of the raw bytes when `addr`
/// is a registered typed array / Buffer / ArrayBuffer, else `None` so the caller
/// falls back to a StringHeader read. A Buffer/Uint8Array body fed straight to
/// `string_from_header` read its byte length off the right field but its data
/// off the StringHeader data offset (20) instead of the buffer data offset (8),
/// shifting every binary body left by 12 bytes (#5483, the Request-side twin of
/// #5435's zero-fill).
pub(crate) unsafe fn body_addr_buffer_bytes(addr: usize) -> Option<Vec<u8>> {
if addr < 0x1000 {
return None;
}
Expand Down
7 changes: 5 additions & 2 deletions crates/perry-stdlib/src/fetch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1120,7 +1120,10 @@ fn headers_from_header_map(headers: &reqwest::header::HeaderMap) -> HeadersStore
struct RequestRecord {
url: String,
method: String,
body: Option<String>,
/// Raw body bytes, stored verbatim so a binary (Buffer/Uint8Array) body
/// survives byte-for-byte through `arrayBuffer()`/`text()` (#5483). `text()`
/// / `json()` still decode lossily via `from_utf8_lossy`, matching Node.
body: Option<Vec<u8>>,
body_used: bool,
headers: HeadersStore,
destination: String,
Expand Down Expand Up @@ -1849,7 +1852,7 @@ fn consume_request_body(handle: f64) -> Result<Vec<u8>, &'static str> {
return Err(BODY_ALREADY_USED_MESSAGE);
}
req.body_used = true;
Ok(body.into_bytes())
Ok(body)
}

/// request.text() -> Promise<string>. Mirrors `js_fetch_response_text`: the
Expand Down
13 changes: 12 additions & 1 deletion crates/perry-stdlib/src/fetch/request_ctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,18 @@ pub unsafe extern "C" fn js_request_new(
throw_fetch_type_error(&format!("'{raw_method}' HTTP method is unsupported."));
}
let method = normalize_method(&raw_method);
let body = string_from_header(body_ptr);
// A Buffer / Uint8Array / typed-array / ArrayBuffer body reaches us as a
// BufferHeader/TypedArrayHeader pointer (codegen ran the value through
// `js_get_string_pointer_unified`), NOT a StringHeader — the same for both
// the static-literal path and `js_request_new_from_init`. Reading it via
// `string_from_header` took the byte length off the right field but the data
// off the StringHeader data offset (20) instead of the buffer data offset
// (8), shifting every binary body left by 12 bytes (#5483). Probe the
// typed-array/buffer registries first and copy the real bytes verbatim; a
// genuine string body falls through to the lossless StringHeader read so its
// UTF-8 bytes are preserved.
let body: Option<Vec<u8>> = dispatch::body_addr_buffer_bytes(body_ptr as usize)
.or_else(|| dispatch::body_bytes_from_header(body_ptr));
// GET/HEAD requests may not carry a body (WHATWG fetch). Refs #2643.
if body.is_some() && (method == "GET" || method == "HEAD") {
throw_fetch_type_error("Request with GET/HEAD method cannot have body.");
Expand Down
28 changes: 28 additions & 0 deletions test-files/test_gap_request_binary_body_5483.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Test new Request(url, { body }) with a Buffer/Uint8Array body round-trips
// without the +12-byte offset that dropped the first 12 bytes (#5483).
// Expected output:
// string 16: len=16 "ABCDEFGHIJKLMNOP"
// buffer 16: len=16 "ABCDEFGHIJKLMNOP"
// buffer 40: len=40 "0123456789012345678901234567890123456789"
// uint8 6 : len=6 "ABCDEF"
// arraybuffer bytes: [255,0,137,80,78,71]

async function tb(label: string, body: any): Promise<void> {
const req = new Request("http://h/x", { method: "POST", body });
const t = await req.text();
console.log(`${label}: len=${t.length} ${JSON.stringify(t)}`);
}

async function main(): Promise<void> {
await tb("string 16", "ABCDEFGHIJKLMNOP");
await tb("buffer 16", Buffer.from("ABCDEFGHIJKLMNOP"));
await tb("buffer 40", Buffer.from("0123456789012345678901234567890123456789"));
await tb("uint8 6 ", new Uint8Array([65, 66, 67, 68, 69, 70])); // "ABCDEF"

// Non-UTF-8 bytes must survive verbatim through arrayBuffer().
const bin = new Uint8Array([255, 0, 137, 80, 78, 71]);
const req = new Request("http://h/x", { method: "POST", body: bin });
const got = new Uint8Array(await req.arrayBuffer());
console.log("arraybuffer bytes: " + JSON.stringify(Array.from(got)));
}
void main();
Loading