Summary
The /repo-assets/[...path] route handler is dynamically rendered and reads files from outside the Next.js project directory (process.cwd()/../assets) at request time. On a standard serverless deploy (Vercel/Lambda), files outside web/ are not included in the function bundle and the computed path cannot be traced, so every locally-resolved asset (stats banner, gallery photos) returns 404 in production. The production build already emits a file-tracing (NFT) warning for exactly this route.
Location
web/app/repo-assets/[...path]/route.ts:5-6, :20-33
- Callers that route local assets here:
web/lib/parse-readme.ts:105-116 (resolveAssetSrc)
Evidence
const REPO_ROOT = path.join(process.cwd(), "..");
const ASSETS_DIR = path.join(REPO_ROOT, "assets");
// ...
const filePath = path.join(ASSETS_DIR, ...segments);
const resolved = path.resolve(filePath);
// ...
const body = fs.readFileSync(resolved);
Production build warning (next build):
Turbopack build encountered 1 warnings:
./web/next.config.ts
Encountered unexpected file in NFT list ... the whole project was traced unintentionally ...
- filesystem operations (like path.join, path.resolve or fs.readFile) ...
Import trace: App Route: ... app/repo-assets/[...path]/route.ts
Route render mode from the build table: ƒ /repo-assets/[...path] (Dynamic, server-rendered on demand), while all five pages are ○ (Static).
Impact
On a serverless target, process.cwd()/.. is not the repo root at runtime and ../assets/* is not bundled → the stats banner and gallery images (which resolveAssetSrc deliberately routes through this handler when the local file exists) silently break (404) in production. Works only on a long-running Node server started with next start from web/ where the sibling assets/ dir is present.
Recommended fix
Stop serving repo-root assets through a runtime route in production. Options:
- Copy the needed assets into
web/public/ at build time (or import them so tracing bundles them) and reference them directly.
- If the route stays: pin
export const runtime = "nodejs", ensure the assets are traced via outputFileTracingIncludes, and add a build step copying ../assets → web/public/repo-assets.
Route hardening to include while here:
- Add
X-Content-Type-Options: nosniff to responses (route.ts:35-39).
- Add a
fs.realpathSync containment re-check (the current path.resolve + startsWith(ASSETS_DIR + sep) guard blocks .. traversal, but symlinks planted in assets/ are followed) (route.ts:24-33).
- Switch
fs.readFileSync/statSync/existsSync to fs/promises (sync I/O blocks the event loop) and lengthen Cache-Control beyond max-age=60 for immutable assets (route.ts:38).
Acceptance criteria
Summary
The
/repo-assets/[...path]route handler is dynamically rendered and reads files from outside the Next.js project directory (process.cwd()/../assets) at request time. On a standard serverless deploy (Vercel/Lambda), files outsideweb/are not included in the function bundle and the computed path cannot be traced, so every locally-resolved asset (stats banner, gallery photos) returns 404 in production. The production build already emits a file-tracing (NFT) warning for exactly this route.Location
web/app/repo-assets/[...path]/route.ts:5-6,:20-33web/lib/parse-readme.ts:105-116(resolveAssetSrc)Evidence
Production build warning (
next build):Route render mode from the build table:
ƒ /repo-assets/[...path](Dynamic, server-rendered on demand), while all five pages are○(Static).Impact
On a serverless target,
process.cwd()/..is not the repo root at runtime and../assets/*is not bundled → the stats banner and gallery images (whichresolveAssetSrcdeliberately routes through this handler when the local file exists) silently break (404) in production. Works only on a long-running Node server started withnext startfromweb/where the siblingassets/dir is present.Recommended fix
Stop serving repo-root assets through a runtime route in production. Options:
web/public/at build time (orimportthem so tracing bundles them) and reference them directly.export const runtime = "nodejs", ensure the assets are traced viaoutputFileTracingIncludes, and add a build step copying../assets→web/public/repo-assets.Route hardening to include while here:
X-Content-Type-Options: nosniffto responses (route.ts:35-39).fs.realpathSynccontainment re-check (the currentpath.resolve+startsWith(ASSETS_DIR + sep)guard blocks..traversal, but symlinks planted inassets/are followed) (route.ts:24-33).fs.readFileSync/statSync/existsSynctofs/promises(sync I/O blocks the event loop) and lengthenCache-Controlbeyondmax-age=60for immutable assets (route.ts:38).Acceptance criteria
next dev).next buildno longer emits the whole-project NFT trace warning for the asset route.nosniff; symlink escape is blocked; reads are async.