fix(fetch): #234 real Blob with arrayBuffer/text/bytes/slice instance methods#238
Closed
TheHypnoo wants to merge 2 commits into
Closed
fix(fetch): #234 real Blob with arrayBuffer/text/bytes/slice instance methods#238TheHypnoo wants to merge 2 commits into
TheHypnoo wants to merge 2 commits into
Conversation
…nstance methods
Pre-fix `await response.blob()` returned a metadata-only stub `{size, type}`
and silently dropped `resp.body`. Calls to `blob.arrayBuffer()` / `.text()` /
`.bytes()` / `.slice()` got nothing because no codegen dispatch arms existed.
Three-part fix:
1. Runtime (perry-stdlib/src/fetch.rs): new BlobData + BLOB_REGISTRY +
alloc_blob mirroring HEADERS_REGISTRY/alloc_headers. js_response_blob
rewritten to clone body bytes + content-type into a fresh BlobData,
register it, resolve the promise with the numeric handle as f64. 6 new
FFIs: js_blob_size, js_blob_type, js_blob_array_buffer (allocates real
BufferHeader same shape as PerryTS#232's response.arrayBuffer fix),
js_blob_bytes (alias), js_blob_text (UTF-8 lossy decode matching WHATWG
replacement-character semantics), js_blob_slice with NaN sentinels for
missing numeric args, null type_ptr to inherit, negative-index
normalisation per WHATWG Blob spec.
2. HIR (perry-hir/src/destructuring.rs): two new lower_var_decl arms next
to the existing Response.clone() propagation. `const blob = await
<recv>.blob()` where <recv> is fetch::Response → register blob::Blob.
`const b2 = <recv>.slice(...)` where <recv> is blob::Blob → propagate
so chained slicing keeps Blob-ness. Existing native-instance dispatch
in expr_call.rs / expr_member.rs then routes .text() / .size / etc. to
NativeMethodCall { module: "blob" } automatically.
3. Codegen (perry-codegen/src/lower_call.rs + runtime_decls.rs): new
`if module == "blob"` arm after the existing fetch arm, dispatching
size → DOUBLE return, type → STRING_TAG NaN-box, arrayBuffer/bytes/text
→ Promise pointer + nanbox_pointer_inline, slice → 0-3 args with
double_literal(f64::NAN) sentinels. 6 new declare_function rows.
Test: test-files/test_issue_234_blob_methods.ts covers 11 cases (size,
text, arrayBuffer + Uint8Array roundtrip, bytes alias, multi-byte UTF-8 +
Buffer.from roundtrip, slice with various arg shapes, empty body, negative
indices) — all match `node --experimental-strip-types` byte-for-byte.
`blob.stream()` deferred to PerryTS#237 (Web Streams API + ReadableStream needed
project-wide before scoping a Blob-only stub).
Closes PerryTS#234
Refs PerryTS#237 (blob.stream() follow-up)
The new regression test from issue PerryTS#234 includes a Buffer.from(blob.arrayBuffer()) round-trip in the multi-byte-UTF-8 case. Same macOS-14 SDK/linker pattern as test_gap_buffer_ops / test_buffer_small_alloc / test_issue_227_array_buffer_bytes / test_gap_fetch_response — compiles and runs byte-for-byte against Node on local macOS 15.x+, only the macOS-14 GitHub runner is affected. Add to SKIP_TESTS in compile-smoke (.github/workflows/test.yml) and to known_failures.json as ci-env, mirroring the v0.5.354 fold-in pattern for PerryTS#232.
proggeramlug
added a commit
that referenced
this pull request
Apr 28, 2026
… cleanup Folds PR #238 (TheHypnoo, closes #234 — real Blob with arrayBuffer/text/ bytes/slice instance methods) on top of v0.5.358. Two correctness/cleanup edits applied during merge: (a) crates/perry-stdlib/src/fetch.rs::js_blob_slice — when type_ptr is null, default the new blob's content_type to String::new() instead of inheriting the original blob's type. Per WHATWG Blob spec / MDN: when contentType is absent, the new blob's type is the empty string. Caught by edge audit (Node returns "" for `b.slice(0, 1).type` when called without a type arg, original PR returned the inherited type). Same fix to the inner string_from_header(...).unwrap_or(...) decode-failure fallback. (b) crates/perry-codegen/src/lower_call.rs slice arm had a duplicate comment block restating the NaN-sentinel rationale twice. Collapsed into one block. Bumped version 0.5.358 → 0.5.359 (above origin's parallel-track v0.5.357 and v0.5.358 which landed since the PR's 0.5.356 base). Refs #237 (blob.stream() Web Streams follow-up).
Contributor
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.
Summary
Closes #234.
Pre-fix
await response.blob()returned a metadata-only stub{size, type}and silently droppedresp.body. Calls toblob.arrayBuffer()/.text()/.bytes()/.slice()got nothing because no codegen dispatch arms existed.Approach
Three-part fix mirroring the precedent from #232 (real
BufferHeaderforresponse.arrayBuffer) and #227 (registered-bufferis_registered_bufferplumbing):1. Runtime —
crates/perry-stdlib/src/fetch.rsBlobData { body, content_type }+BLOB_REGISTRY: Mutex<HashMap<usize, BlobData>>+NEXT_BLOB_ID+alloc_blobhelper, mirroring theHEADERS_REGISTRY/alloc_headerspattern in the same file.js_response_blobrewritten: clones body bytes + content-type into a freshBlobData, registers it, resolves the promise with the numeric handle asf64. Matches the Response-handle ABI documented atlower_call.rs:2788.js_blob_size(handle) -> f64js_blob_type(handle) -> *mut StringHeader(codegen NaN-boxes withSTRING_TAG)js_blob_array_buffer(handle) -> *mut Promise— allocates a realBufferHeaderviaperry_runtime::buffer::buffer_alloc(len), memcpys body, NaN-boxes asPOINTER_TAG. Same shape asjs_response_array_bufferfrom fix(fetch): copy body bytes into response.arrayBuffer() result #232 sonew Uint8Array(buf)andBuffer.from(buf)see real bytes via theis_registered_bufferpath from response.arrayBuffer() returns metadata-only object — body bytes never reach TS #227.js_blob_bytes(handle) -> *mut Promise— alias tojs_blob_array_buffer(theBufferHeaderis already byte-array-shaped).js_blob_text(handle) -> *mut Promise— UTF-8 lossy decode toStringHeader, matches WHATWG Blob.text() replacement-character semantics.js_blob_slice(handle, start, end, type_ptr) -> f64— canonicalf64::NANsentinel for missing numeric args, nulltype_ptrto inherit the original content-type. Negative indices count from the end and clamp to[0, len]per WHATWG Blob spec; allocates a fresh blob with sliced bytes.2. HIR —
crates/perry-hir/src/destructuring.rsTwo new arms in
lower_var_declnext to the existing Response.clone() propagation:const blob = await <recv>.blob()where<recv>isfetch::Response→register_native_instance(name, "blob", "Blob").const b2 = <recv>.slice(...)where<recv>isblob::Blob→ propagate the type so chained slicing keeps Blob-ness.The existing native-instance method-call lowering at
expr_call.rs:723-740and property access atexpr_member.rs:312-326then route subsequent.text()/.size/ etc. toExpr::NativeMethodCall { module: "blob", method: ..., args: [] }automatically — no other HIR changes needed.3. Codegen —
crates/perry-codegen/src/lower_call.rs+runtime_decls.rsNew
if module == "blob"arm right after the existingif module == "fetch"block:size→js_blob_size(DOUBLE return)type→js_blob_type+nanbox_string_inlinearrayBuffer/bytes/text→ respective Promise-returning FFIs +nanbox_pointer_inlineslice→ 0-3 args withdouble_literal(f64::NAN)sentinels for missing numeric args and"0"for missing type pointer; returns the new blob handle asf64directly.6 new
module.declare_function(...)rows inruntime_decls.rswith the matching ABIs.Out of scope
blob.stream()— needs a full WHATWG Web Streams API implementation (ReadableStream,ReadableStreamDefaultReader,Symbol.asyncIteratorprotocol for streams, eventuallyWritableStream+TransformStreamforpipeTo/pipeThrough). That affectsresponse.body/Request.body/new Response(stream)too, so a Blob-only stub would create inconsistency. Tracked as Web Streams API: implement ReadableStream + blob.stream() / response.body #237.response.formData()— separate gap; not requested by response.blob() drops body bytes — same shape as #227, needs Blob instance methods #234.Test plan
test-files/test_issue_234_blob_methods.tscovers 11 cases: size, text, arrayBuffer + Uint8Array roundtrip, bytes alias, multi-byte UTF-8 +Buffer.fromroundtrip,slice(start, end),slice()(no args, full clone),slice(start)(defaults end to length),slice(0, 3, "text/plain")(typed slice), empty body, negative slice indices. All matchnode --experimental-strip-typesbyte-for-byte.test-files/test_issue_227_array_buffer_bytes.ts(the response.arrayBuffer() returns metadata-only object — body bytes never reach TS #227 regression) continues to match Node byte-for-byte.test-files/test_gap_fetch_response.tscontinues to match Node modulo the pre-existing experimental-strip-types preamble.cargo build --release -p perry-runtime -p perry-stdlib -p perryclean.Refs
blob.stream()+ Web Streams API)response.arrayBuffer()body bytes) and fix(fetch): copy body bytes into response.arrayBuffer() result #232 (realBufferHeaderfor response.arrayBuffer)