Skip to content

fix(react-email): don't leak a file handle when a static file can't be read - #3812

Merged
dielduarte merged 3 commits into
resend:canaryfrom
b84330808:fix/static-file-handle-leak
Nov 9, 2026
Merged

dielduarte merged 3 commits into
resend:canaryfrom
b84330808:fix/static-file-handle-leak

Conversation

@b84330808

@b84330808 b84330808 commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #3808.

email dev serves files under static/ with serveStaticFile(). It opened the file with fs.open() and only closed the handle after a successful read, so when the read fails after the open succeeds (a request for a directory under /static on Linux or macOS gives EISDIR; a file another program has locked on Windows gives EBUSY) the FileHandle is never closed.

Node.js 24 only prints a DEP0137 warning for that. Node.js 25 made it an error, so the preview server exits the next time the garbage collector runs:

Error: A FileHandle object was closed during garbage collection. This used to be allowed with a deprecation warning but is now considered an error. Please close FileHandle objects explicitly. File descriptor: 3 (...\emails\static\logo.png)
  code: 'ERR_INVALID_STATE'

Changes:

  • Read the file with fs.readFile(path) instead of opening a handle first, so nothing stays open when the read fails. The 404/500 handling is unchanged.
  • Add a test that makes the read fail and checks no opened handle is left open. It fails without the fix.
  • Add a patch changeset for react-email.

Reproduction on Windows with Node.js 26.12: with emails/static/logo.png locked by another process, calling serveStaticFile() and then forcing GC crashes the process before this change; after it, the request gets a 500 and the process keeps running.

I used Claude Code to write this change.


Summary by cubic

Fixes #3808 and a related leak in @react-email/ui. email dev served files under static/ by opening a FileHandle and only closing it after a successful read; when the read failed (a directory request on Linux/macOS gives EISDIR, a locked file on Windows gives EBUSY), the handle leaked. Node.js 24 prints a DEP0137 warning for that, but Node.js 25 turns it into an ERR_INVALID_STATE error that crashes the preview server at the next garbage collection. The same pattern leaked a handle while scanning the emails directory, and an unreadable file there failed the whole scan.

Changes:

  • Reads static files with fs.readFile() instead of opening a handle first, so nothing stays open when the read fails; the 404/500 handling is unchanged, and requesting a directory under /static now returns a 404 instead of a 500.
  • Scans the emails directory with fs.promises.stat() and reads by path, skipping unreadable files with a warning instead of failing the scan.
  • Adds tests that make reads fail and verify no descriptor stays open; they fail without the fix.
  • Adds patch changesets for react-email and @react-email/ui.

Written for commit 0b05a8b. Summary will update on new commits.

View guided diff

@vercel

vercel Bot commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

@b84330808 is attempting to deploy a commit to the resend Team on Vercel.

A member of the Team first needs to authorize it.

@changeset-bot

changeset-bot Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0b05a8b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
react-email Patch
@react-email/ui Patch
@react-email/editor Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added the linear-synced PR has been synced to Linear label Oct 28, 2026
@vercel

vercel Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
react-email Ready Ready Preview Nov 9, 2026 8:28pm UTC
react-email-demo Ready Ready Preview Nov 9, 2026 8:28pm UTC

Request Review

@pkg-pr-new

pkg-pr-new Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/react-email@3812
npm i https://pkg.pr.new/@react-email/ui@3812

commit: 0b05a8b

b84330808 and others added 3 commits November 9, 2026 17:24
…e read

serveStaticFile() opened a FileHandle and only closed it after a
successful read. When the read failed (a directory on Linux/macOS, a
locked file on Windows), the handle was left for the garbage collector,
which Node.js 25+ turns into an ERR_INVALID_STATE crash of `email dev`.
Read the file by path instead so nothing is left open.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Test the static file server without mocking fs: request a real
directory, check that no file descriptor stays open, and move
setup/cleanup to beforeEach/afterEach.

@dielduarte dielduarte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for tracking this down, and for the clear write-up and repro in #3808! The fs.readFile(path) fix is the right one.

I pushed a few things on top and rebased onto the latest canary:

  • Directories under /static now return 404 instead of 500. A request like /static/ used to log a stack trace and send a 500. It now gets the same 404 as a missing file.
  • Tests without mocking fs. I replaced the test that spied on fs.promises.open with one that requests a real directory and checks that no file descriptor is left open. It fails on the old code. It's skipped on Windows, which has no /dev/[fd](https://github.com/dev/fd). I also added a test for a missing file and moved setup and cleanup into beforeEach/afterEach, so a failing assertion can't leave process.cwd mocked for the rest of the file.
  • The same leak in @react-email/ui. isFileAnEmail in get-emails-directory-metadata.ts opened a handle and could leak it if stat() or readFile() threw. It now reads by path, and a file it can't read is skipped with a warning instead of failing the whole scan. It has its own changeset.

@dielduarte
dielduarte merged commit 44169a6 into resend:canary Nov 9, 2026
14 checks passed

This branch was successfully deployed

2 active deployments
Preview – react-email-demo — 0b05a8bb Deployed Nov 9, 2026 by vercel[bot]
Preview – react-email — 0b05a8bb Deployed Nov 9, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

linear-synced PR has been synced to Linear

Projects

None yet

Development

Successfully merging this pull request may close these issues.

email dev crashes on Node.js 25+ when a file under static/ can't be read

2 participants