`fs.openAsBlob()` reads the file's new contents after it is modified instead of rejecting as Node does
What version of Bun is running?
1.4.2+744846f84 (also reproduced on 1.3.14+0d9b296af)
What platform is your computer?
Darwin 25.6.0 arm64 arm
What steps can reproduce the bug?
Bun implements fs.openAsBlob() as Promise.resolve(Bun.file(path, options)), so every read of the returned Blob returns the path's current contents. Node documents the opposite contract:
The file must not be modified after the
Blobis created. Any modifications will cause reading theBlobdata to fail with aDOMExceptionerror. Synchronous stat operations on the file when theBlobis created, and before each read in order to detect whether the file data has been modified on disk.
Save as repro.mjs and run node repro.mjs, then bun repro.mjs:
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const file = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "openAsBlob-")), "a.txt");
fs.writeFileSync(file, "hello");
const blob = await fs.openAsBlob(file);
fs.writeFileSync(file, "swapped!"); // modify after the Blob is created
console.log("runtime:", typeof Bun === "undefined" ? `node ${process.version}` : `bun ${Bun.version}`);
console.log("blob.size:", blob.size);
try {
console.log("text():", JSON.stringify(await blob.text()));
} catch (err) {
console.log("text() rejected:", err.name, "-", err.message);
}
What is the expected behavior?
What Node v22.22.3 prints, per the docs quoted above:
runtime: node v22.22.3
blob.size: 5
text() rejected: NotReadableError - The blob could not be read
What do you see instead?
runtime: bun 1.4.2
blob.size: 8
text(): "swapped!"
Additional information
More cases from the same cause. In each, Node rejects the read with NotReadableError from text(), arrayBuffer(), stream(), and slice().text() alike.
- After a same-length overwrite with
"HELLO", Bun returns"HELLO". Node still rejects, so its check is not on size alone. - If
blob.sizeis read before the write, it stays 5 afterwards.text()then returns"swapp", the new contents cut to the old size, andblob.slice(1, 4).text()returns"wap". - With
sizeread first and the file then truncated to"hi",text()returns"hi"whilesizestays 5.
A missing path differs too: await fs.openAsBlob("/nonexistent") resolves with a size-0 Blob on Bun, and Node rejects with ERR_INVALID_ARG_VALUE ("Unable to open file as blob").
Code that calls openAsBlob() to get a replayable request body, for example to retry a multipart upload, depends on this check. If the file changed between attempts, the retry fails on Node, and on Bun it sends bytes that differ from the first attempt's.
Lazy reads are documented for Bun.file(), so this report is only about node:fs. #40339 (MIME type inferred from the extension) comes from the same delegation to Bun.file(). openAsBlob was added in #9628.
Source: oven-sh/bun