Skip to content

lib: add FileAPI support - #67277

Open
avivkeller wants to merge 4 commits into
nodejs:mainfrom
avivkeller:wpt-fileapi
Open

avivkeller wants to merge 4 commits into
nodejs:mainfrom
avivkeller:wpt-fileapi

Conversation

@avivkeller

@avivkeller avivkeller commented Nov 5, 2026 •

Copy link
Copy Markdown
Member

Adds support for the following FileAPI interfaces:

  • FileReader
  • FileReaderSync
  • ProgressEvent

Signed-off-by: avivkeller <me@aviv.sh>
@avivkeller
avivkeller requested a review from panva November 5, 2026 15:47
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/startup
  • @nodejs/web-standards

@nodejs-github-bot nodejs-github-bot added lib / src Issues and PRs involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run. labels Nov 5, 2026
@codecov

codecov Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.47116% with 60 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.68%. Comparing base (3b209b2) to head (cd0e0fb).
⚠️ Report is 30 commits behind head on main.

Files with missing lines Patch % Lines
lib/internal/filereader.js 91.75% 60 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff            @@
##             main   #67277     +/-   ##
=========================================
  Coverage   90.68%   90.68%             
=========================================
  Files         797      799      +2     
  Lines      279367   280530   +1163     
  Branches    53737    53882    +145     
=========================================
+ Hits       253344   254403   +1059     
- Misses      16319    16414     +95     
- Partials     9704     9713      +9     
Files with missing lines Coverage Δ
...internal/bootstrap/web/exposed-window-or-worker.js 94.07% <100.00%> (+0.18%) ⬆️
lib/internal/file.js 97.41% <100.00%> (+0.05%) ⬆️
lib/internal/process/pre_execution.js 96.43% <100.00%> (-0.08%) ⬇️
lib/internal/progress_event.js 100.00% <100.00%> (ø)
lib/internal/webidl.js 100.00% <100.00%> (ø)
lib/internal/webworker.js 95.86% <100.00%> (+0.01%) ⬆️
src/node_options.cc 88.29% <100.00%> (+0.11%) ⬆️
src/node_options.h 95.85% <100.00%> (+0.14%) ⬆️
lib/internal/filereader.js 91.75% <91.75%> (ø)

... and 54 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread lib/internal/filereader.js Resolved

@mcollina mcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Comment thread lib/internal/filereader.js Resolved
@mcollina mcollina added the semver-major PRs that contain breaking changes and should be released in the next major version. label Nov 5, 2026
Signed-off-by: avivkeller <me@aviv.sh>
KhafraDev

This comment was marked as resolved.

@avivkeller avivkeller added author ready PRs with CI started, the required approvals, and no outstanding review comments. commit-queue PRs queued for automated landing through the Commit Queue. request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. and removed commit-queue PRs queued for automated landing through the Commit Queue. labels Nov 5, 2026
Comment thread doc/api/globals.md Resolved
Comment thread doc/api/globals.md Resolved
Comment thread lib/internal/filereader.js Resolved
Comment thread lib/internal/filereader.js Resolved
Comment thread lib/internal/filereader.js Resolved
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. label Nov 5, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Comment thread lib/internal/filereader.js Resolved
Comment thread lib/internal/filereader.js Resolved

@jasnell jasnell left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please mark initially as experimental.

@panva panva removed the author ready PRs with CI started, the required approvals, and no outstanding review comments. label Nov 5, 2026
Comment thread doc/api/globals.md Resolved
Comment thread doc/api/globals.md Outdated
A browser-compatible implementation of {FileReaderSync} from the [File API][].
It is exposed only inside a [Web Worker][] (`--experimental-web-worker`),
matching `[Exposed=(DedicatedWorker,SharedWorker)]`. It is not available on
the main thread or in threads created with `node:worker_threads`.

@jasnell jasnell Nov 5, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I question whether this restriction is actually necessary in Node.js. Specifically the "It is not available on the main thread" or in worker threads.

The restrictions that apply to the browser do not necessarily apply here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was marked resolved but it's still limited to web workers. The question is still open here.

Comment thread doc/api/globals.md Resolved
Comment thread test/parallel/test-filereader.js Resolved
Comment thread lib/internal/filereader.js Resolved
@jasnell

jasnell commented Nov 5, 2026

Copy link
Copy Markdown
Member

A separate AI-agent review flagged the following additional items:

  1. Progress events always report loaded = 0, total = 0, lengthComputable = false.
  • fireAProgressEvent() just does new ProgressEvent(name).
  • The File API spec's own example computes evt.loaded / evt.total. With this PR that gives NaN.
  • Chrome and Firefox fill in these values, and progress-bar code depends on them.
  • Fix: pass the bytes read so far and blob.size, as the XHR "fire a progress event" definition does.
  1. Events have isTrusted === false.
  • Other Node code marks events it fires itself as trusted with kTrustEvent (for example abort_controller.js and perf/performance.js).
  • Here that can't happen, because convertProgressEventInit drops the symbol before calling super().
  • Fix: let kTrustEvent pass through to Event, or set it after construction.
  1. The reader can get stuck in LOADING forever.
  • readOperation sets the state to 'loading' before it calls the public blob.stream(), getReader() and read() methods.
  • If a Blob subclass or patched prototype makes stream() throw, readyState stays at 1. Every later read then throws InvalidStateError (confirmed).
  • Fix: read through the blob's internal handle, as getBlobDataSync already does.
  • Related: it reads the public blob.type getter, so a subclass can change the data URL's MIME type (confirmed data:text/evil;...). Use the exported internal kType instead.
  1. loadend is fired in a separate task instead of the same one.
  • The spec fires loadend in the same task as load/error, right after it.
  • queueLoadEnd() schedules another setImmediate, so other code can run in between. I confirmed that a setImmediate scheduled inside onload runs before loadend.
  • The generation counter already handles a new read started from onload, so this can simply be inlined. The same applies to settleWithError.
  1. Data URLs for blobs with no type differ from browsers.
  • The PR follows the spec's literal wording and produces data:;base64,....
  • The WPT tests and browsers produce data:application/octet-stream;base64,.... The PR marks those two WPT tests as expected failures. (My note: this is one of the discrepancies between "browser-compatible" and "spec-compatible" that I noted in my review)
  • Choosing to break from both WPT and every browser deserves an explicit decision and probably an upstream spec issue. Otherwise, matching WPT seems better.
  1. "Replacement" encodings cause an error instead of decoding.
  • readAsText(blob, 'iso-2022-kr') (and similar labels) fires error with ERR_ENCODING_NOT_SUPPORTED.
  • The spec says to decode with the replacement decoder, which returns "\uFFFD" for non-empty input.
  • Builds without ICU will similarly fire error for any encoding other than UTF-8/UTF-16LE/latin1. I didn't test this, but it's probably worth documenting.
  1. FileList can't be created by users. createFileList is only used by tests, so the global exists just to satisfy interface-exposure checks. Is it actually worth shipping if it cannot be used?

@jasnell jasnell added the experimental Issues and PRs related to experimental features. label Nov 5, 2026
Signed-off-by: avivkeller <me@aviv.sh>
Comment thread doc/api/globals.md Outdated
added: REPLACEME
-->

> Stability: 1 - Experimental

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1.0 Active development?

@panva panva left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this now be semver-minor with the new globals behind a default-off experimental flag?

The empty-MIME data URL results follow the current package-data wording, while WPT expects application/octet-stream. Progress counter initialization is also underspecified; that leaves an interoperability decision rather than a requirement to always report zero.

//
// "Event handler for the load or error events could have started another
// load, if that happens the loadend event for this load is not fired."
queueFileReadingTask(fr, generation, () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The read operation fires loadend in the same task as load/error. Scheduling another task here lets a setImmediate scheduled inside onload start a second read and suppress the first read's loadend.

Please perform the state check and dispatch within the completion task.

const reader = new FileReader();
reader.onload = () => setImmediate(() => console.log('immediate'));
reader.onloadend = () => console.log('loadend');
reader.readAsText(new Blob(['x']));

Prints immediate then loadend; expected loadend then immediate.

// "Let promise be the result of reading all bytes from stream with
// reader."
// "Wait for promise to be fulfilled or rejected."
const buffer = getBlobDataSync(blob);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

undefined here means this helper cannot obtain the bytes synchronously, not that the Blob is unreadable. All four methods consequently throw NotReadableError for a healthy fs.openAsBlob() result that FileReader reads successfully.

The synchronous methods must wait for the read and return its bytes. Can we support file-backed Blobs through a synchronous read path?

import { openAsBlob } from 'node:fs';
import { isMainThread } from 'node:worker_threads';

if (isMainThread) {
  const worker = new Worker(import.meta.url, { type: 'module' });
  worker.onmessage = ({ data }) => console.log(data);
} else {
  const blob = await openAsBlob(import.meta.filename);
  try {
    postMessage(new FileReaderSync().readAsText(blob));
  } catch (err) {
    postMessage(err.name);
  }
  close();
}

Prints NotReadableError; expected the source file contents.

// "5. Let stream be the result of calling get stream on blob."
let stream;
try {
stream = createBlobReaderStream(blob[kHandle].getReader());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This helper collapses native file failures into NotReadableError. fs.openAsBlob(path), followed by deleting the file and calling readAsText(blob), reproduces that error.

File API §7.1 requires NotFoundError when the resource cannot be found. Please preserve the underlying failure reason through the Blob plumbing so this API can expose the required exception.

import { openAsBlob, unlinkSync, writeFileSync } from 'node:fs';

const path = new URL(`./filereader-repro-${process.pid}.txt`, import.meta.url);
writeFileSync(path, 'x');
const blob = await openAsBlob(path);
unlinkSync(path);
const reader = new FileReader();
reader.onerror = () => console.log(reader.error.name);
reader.readAsText(blob);

Prints NotReadableError; expected NotFoundError.

Comment thread lib/internal/webworker.js
exposeInterface(globalThis, 'WorkerNavigator', WorkerNavigator);
exposeInterface(globalThis, 'WorkerLocation', WorkerLocation);
// [Exposed=(DedicatedWorker,SharedWorker)]
exposeLazyInterfaces(globalThis, 'internal/filereader', ['FileReaderSync']);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please gate this on --experimental-fileapi. Worker preparation removes the File API globals when the flag is disabled, but this runs afterward and unconditionally reinstalls FileReaderSync. A Web Worker started with only --experimental-web-worker therefore still gets the experimental interface.

const worker = new Worker(
  'data:text/javascript,postMessage(typeof FileReaderSync);close()',
);
worker.onmessage = ({ data }) => console.log(data);

Prints function; expected undefined without --experimental-fileapi.

constructor(type, eventInitDict = kEmptyObject) {
const prefix = "Failed to construct 'ProgressEvent'";
requiredArguments(arguments.length, 1, { prefix });
const trusted = eventInitDict != null &&

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reads argument 2 before converting argument 1, contrary to Web IDL's conversion order. With Symbol() as type and a Proxy dictionary whose get trap throws a sentinel, construction throws the sentinel instead of the required TypeError.

Please convert type first and keep internal trust handling outside the public dictionary conversion.

new ProgressEvent(Symbol(), new Proxy({}, {
  get() { throw new Error('argument 2 was read'); },
}));

Throws Error: argument 2 was read; expected TypeError before the dictionary is accessed.

if (mimeType === '') {
return `data:;base64,${encoded}`;
}
return `data:${mimeType};base64,${encoded}`;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Data URL serialization needs another look for MIME types containing URL delimiters. text/plain;x=# currently produces an unfetchable URL. RFC 2397 §3 calls for escaping where necessary, although the exact File API serialization algorithm remains unresolved.

const reader = new FileReader();
reader.onload = () => fetch(reader.result).catch(console.log);
reader.readAsDataURL(new Blob(['hello'], { type: 'text/plain;x=#' }));

This logs TypeError: fetch failed; the generated URL cannot be fetched back to the original bytes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

experimental Issues and PRs related to experimental features. lib / src Issues and PRs involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run. semver-major PRs that contain breaking changes and should be released in the next major version.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants