How a missing @ in a filename broke my Netlify build
It was fine three months ago :(
Three months ago I deployed this Qwik site to Netlify. And after some while, this week I pushed an unrelated content change and the build died without touching a thing on it’s codebase:
No version bumps in package.json. No config changes. The build itself (client, server) compiled cleanly. If it worked three months ago with the exact same versions, why was it broken now?
I did the usual steps: Cleared cache and ran the build, failed. But build process worked fine on local machine. Upgraded all dependencies with ncu to their latest versions, upload, still failed. Until I created a brand new Qwik hello-world project, pushed it to Netlify. Still failed, that’s when I know there must be something wrong on Netlify’s side.
Searching online for "qwik netlify build error ENOENT file not found" wasn't really helpful, found only one forum post describing the same issue. No replies. Auto-closed after seven days. So I posted on Netlify's answers forum and hope someone could give a solution. A response did come eventually, but hours later.The site was down and needed to come back up ASAP, so I wasn't going to sit there refreshing a forum tab. I started digging deep.
The First Clue: A Missing @
I noticed the failure was in the Edge Functions bundling step, an additional step after everything else was done. I opened the output directory on my local machine expecting to see the missing file:
That's when it gets interesting, the file existed. It just had an @ in front of it. Yet the error occurs when attempting to call stat on a file with a filename that is almost identical, except for the missing @?
If the bundler knew this file existed, why was it looking for a file with the wrong name?
Something was stripping the @ somewhere in the process.
Following the Trail
I pulled down the source of @netlify/build from Netlify’s GitHub repo including the package edge-bundler, which responsible for that Edge Functions bundling step. I followed the call chain:
edge_functions/index.ts → bundler.ts → formats/tarball.ts
In tarball.ts, I found the function file list being built:
the code packs bundled edge functions into a tarball for deployment. It calls tar.create() with a list of filenames. And at that point, the filenames still had their @ prefix intact. The correct name was being passed in. So the issue wasn't in the path calculation. The @ was being stripped inside tar.create() itself.
Reproducing the Bug
To confirm, I put together a minimal reproduction with some help from Claude:
And i got:
The bug was confirmed: node-tar WAS doing something on filenames that result in the removal of the @ symbol.
Root Cause: bsdtar (libarchive)’s @archive syntax
After some digging online, bsdtar/libarchive has a feature where entries starting with @ have special meaning: @archive.tar means "open that archive and include its contents". node-tar, as a tar implementation, inherited this behavior. When it sees a filename starting with @ in the files list, it strips the @ symbol and tries to open the remainder as a tar archive to read entries from.
So when node-tar anything starts with a @ symbol:
→ Interpret this as an archive-include directive
→ It tried to open qwik-city-not-found-paths.js as a tar archive
→ Run stat on qwik-city-not-found-paths.js
→ ENOENT
The Fix
In tarball.ts, where the files array is passed to tar.create(), prefix each entry with ./:
The Chain Reaction
After the PR was merged into @netlify/build, the maintainer traced the issue further upstream and submitted a PR to node-tar itself — fixing the error handling so that errors from @-prefixed entries are properly catchable instead of becoming unhandled promise rejections that the caller couldn't catch.
Moreover, somehow it also uncovered a CI coverage gap: the test suite for tarball handling had a describe block that was being silently skipped because of a wrong Deno version number in the test configuration. An entire set of tarball tests had been passing CI without actually running.
One missing @, and suddenly there were three fixes flowing in different directions.
Looking Back
Reading someone else's TypeScript to find out why my site wouldn't deploy is not something I'd done before this week. The typical workaround for this kind of issue you would find online, is to duplicate the files without the @. That works. Your build passes. You move on. But somewhere the same bug is still waiting for the next person. And the next. Until someone reads the function that's eating the @.
There's something I've been thinking about since. The experienced developers who maintain these projects, they do this kind of invisible work all the time, and they do it faster than I ever could. They trace a bug, fix it, and the result is that thousands of people simply never encounter the problem. Nobody knows it was ever there.
I'm not a developer by trade. If these projects had been closed-source, my story would have ended at the forum post with no replies, waiting for someone on the other side to notice. But because the code was right there, I could pull it down and find the answer myself. A workaround solves your problem. A root cause fix solves everyone's.
Links
- Post on netlify forum: Edge Functions bundling failed for Qwik: ENOENT: no such file or directory, stat ‘@qwik-city-not-found-paths.js’ (Vite 7) - #6 by statusbot - Support - Netlify Support Forums
- PR: netlify/build: fix: add ./ prefix to tar entries to handle @ prefixed filenames
- Upstream: node-tar: fix from pieh: error from @ file entry should be catchable
- Upstream: node-tar: where stripping @ symbol as directives: node-tar/src/create.ts at main · isaacs/node-tar
- Bug reproducing repo: osmiumsilver/demo-tar-at-file