Skip to content

[High] Reliability: /repo-assets route reads files outside the project root at request time (breaks on serverless) #43

Description

@Jose-Gael-Cruz-Lopez

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 ../assetsweb/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

  • Stats banner and gallery images load on the production deploy target (verified on a preview deployment, not just next dev).
  • next build no longer emits the whole-project NFT trace warning for the asset route.
  • Asset responses set nosniff; symlink escape is blocked; reads are async.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingreliabilityReliability, error handling, resilienceseverity:highHigh severity

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions