Summary
The existing node:buffer Blob/File implementation covers basic string parts, binary parts, metadata, and object URLs, but the constructors do not apply Node/WHATWG coercion rules for non-binary parts or File metadata. Direct new Blob(...) / new File(...) behavior diverges even though the surface exists.
Node Behavior
Local Node v25.9.0 probe:
(async () => {
const { Blob, File } = require("node:buffer");
const parts = [123, true, false, null, undefined, { toString(){ return "obj"; } }, ["a", "b"]];
const b = new Blob(parts);
console.log("blob text", JSON.stringify(await b.text()));
console.log("blob size", b.size);
const f = new File(parts, 123, { type: "TEXT/PLAIN;Charset=UTF-8", lastModified: "42.9" });
console.log("file name", JSON.stringify(f.name));
console.log("file type", JSON.stringify(f.type));
console.log("file lm", f.lastModified);
console.log("file text", JSON.stringify(await f.text()));
})();
Observed Node behavior:
new Blob(parts).text() resolves to "123truefalsenullundefinedobja,b" and size is 31.
- Non-Blob/non-BufferSource parts are stringified, including arrays via normal array
ToString (["a", "b"] becomes "a,b"), not recursively flattened as raw parts.
new File(parts, 123, ...) coerces the name to "123".
type: "TEXT/PLAIN;Charset=UTF-8" is normalized to "text/plain;charset=utf-8".
lastModified: "42.9" is coerced to numeric 42.9.
Perry Behavior
Current source evidence:
crates/perry-codegen/src/lower_call/builtin.rs lowers new Blob(parts, opts) to js_blob_new(parts, type) and new File(parts, name, opts) to js_file_new(parts, name, type, lastModified).
crates/perry-stdlib/src/fetch_blob.rs::append_blob_part_bytes() only handles strings, Buffer/Uint8Array, Blob handles, and arrays. The comment says anything else is silently dropped, and the array path recurses into elements, so ["a", "b"] becomes "ab" rather than Node's "a,b" stringification.
js_file_new() only reads name and type when the inputs are heap string tags; non-string names become "" instead of ToString(name).
js_blob_new() / js_file_new() store type as provided rather than applying Blob type normalization.
js_file_new() treats lastModified.is_nan() as “use Date.now()”. NaN-boxed non-number values such as a string option take that path instead of ToNumber(lastModified).
Suggested PR Cut
Batch this with related issues in:
node:buffer: Blob and File constructor coercion parity cut
Good batch candidates:
- no other open
node:buffer Blob/File coercion leaf is currently known; keep this as a focused PR unless another constructor-coercion issue appears
Do not batch with:
Acceptance
- parity/regression test proves non-binary Blob parts use Node-compatible
ToString / USVString behavior, including arrays becoming comma-joined strings
- parity/regression test proves
File name coercion matches Node for non-string names
- Blob/File
type normalization matches Node for valid mixed-case MIME strings and invalid characters
lastModified uses Node-compatible numeric coercion and only defaults to current time when the option is absent
- existing Blob/File binary parts, metadata, text/arrayBuffer behavior, and object URL fixtures continue passing
- related known-failure/docs/manifest entries updated if touched
Summary
The existing
node:bufferBlob/File implementation covers basic string parts, binary parts, metadata, and object URLs, but the constructors do not apply Node/WHATWG coercion rules for non-binary parts or File metadata. Directnew Blob(...)/new File(...)behavior diverges even though the surface exists.Node Behavior
Local Node v25.9.0 probe:
Observed Node behavior:
new Blob(parts).text()resolves to"123truefalsenullundefinedobja,b"andsizeis31.ToString(["a", "b"]becomes"a,b"), not recursively flattened as raw parts.new File(parts, 123, ...)coerces the name to"123".type: "TEXT/PLAIN;Charset=UTF-8"is normalized to"text/plain;charset=utf-8".lastModified: "42.9"is coerced to numeric42.9.Perry Behavior
Current source evidence:
crates/perry-codegen/src/lower_call/builtin.rslowersnew Blob(parts, opts)tojs_blob_new(parts, type)andnew File(parts, name, opts)tojs_file_new(parts, name, type, lastModified).crates/perry-stdlib/src/fetch_blob.rs::append_blob_part_bytes()only handles strings, Buffer/Uint8Array, Blob handles, and arrays. The comment says anything else is silently dropped, and the array path recurses into elements, so["a", "b"]becomes"ab"rather than Node's"a,b"stringification.js_file_new()only readsnameandtypewhen the inputs are heap string tags; non-string names become""instead ofToString(name).js_blob_new()/js_file_new()storetypeas provided rather than applying Blob type normalization.js_file_new()treatslastModified.is_nan()as “use Date.now()”. NaN-boxed non-number values such as a string option take that path instead ofToNumber(lastModified).Suggested PR Cut
Batch this with related issues in:
node:buffer: Blob and File constructor coercion parity cutGood batch candidates:
node:bufferBlob/File coercion leaf is currently known; keep this as a focused PR unless another constructor-coercion issue appearsDo not batch with:
node:fs.openAsBlob()file-backed Blob APIFileexposureAcceptance
ToString/ USVString behavior, including arrays becoming comma-joined stringsFilename coercion matches Node for non-string namestypenormalization matches Node for valid mixed-case MIME strings and invalid characterslastModifieduses Node-compatible numeric coercion and only defaults to current time when the option is absent