Skip to content

Return plain text 404 for non-HTML subresource requests to unknown paths - #89

Draft
gaearon wants to merge 4 commits into
fix-not-found-txt-cache-controlfrom
gaearon-not-found
Draft

Return plain text 404 for non-HTML subresource requests to unknown paths#89
gaearon wants to merge 4 commits into
fix-not-found-txt-cache-controlfrom
gaearon-not-found

Conversation

@gaearon

@gaearon gaearon commented Jul 29, 2026

Copy link
Copy Markdown

Companion to vercel/next.js#95930, which uses Sec-Fetch-Dest to serve a plain text 404 for subresource requests (images, fonts, manifests, scripts, etc.) to unknown paths instead of rendering the app's not-found output.

That fix only covers next dev/next start/minimal mode — deployments on this adapter still returned the full prerendered not-found page for these requests, since the CDN routing layer serves it directly without invoking Next.js.

This closes that gap at the routing layer.

Adds a route in the error phase, right after { handle: 'error' } and before the existing not-found dispatch, matching the same Sec-Fetch-Dest values as is-non-html-sec-fetch-dest.ts in vercel/next.js, and serving the shared _next/static/not-found.txt asset with text/plain + 404 instead.

Stacked on #93, which fixes the same missing-cache-control bug on the pre-existing /_next/static/ 404 route and extracts a shared NOT_FOUND_TXT_HEADERS constant. This PR's new route reuses that constant instead of duplicating the header object.

No local unit test — this repo relies on vercel/next.js's e2e suite against real deployments (see #2); coverage comes from not-found-non-document.test.ts and not-found-non-document-dynamic.test.ts, which had isNextDeploy TODOs for exactly this gap.

Verification

The real CI deploy job (NEXT_ENABLE_ADAPTER=1) can't exercise an unpublished branch — it just selects a Vercel team whose build infra runs whatever adapter version is pinned platform-side.

So I ran the actual vercel/next.js Jest deploy suites locally against this branch: packed this branch into a tarball, wired it into not-found-non-document.test.ts and not-found-non-document-dynamic.test.ts via adapterPath + a file: dependency, and ran pnpm test-deploy-turbo for real against fresh Vercel deployments (not scratch curl checks).

The full matrix is static/dynamic fixture × cacheComponents on/off. All four cells go through the new routing rule — I initially wrote off the dynamic/cacheComponents: false cell as "already correct pre-fix," but that was wrong: it hits the same route as the rest, and before the fix the plain-text response's cache-control wasn't actually overridden, it just inherited the CDN's default for the static not-found.txt asset.

cacheComponents: false cacheComponents: true
static fixture had a TODO, needs this fix — 6/6 builds to the same static output as the cell to the left — confirmed by deploying it separately and byte-diffing headers/body against the cacheComponents: false cell, including a path never previously requested
dynamic fixture needs this fix — 3/3 had a TODO, needs this fix (PPR) — 3/3

Manual test plan

Live scratch deployments of this branch (rebuilt with the #93 fix included) for the two matrix rows:

Open either URL with browser dev tools open on the Network tab, then reload. The page loads a missing image, a missing @font-face, and a missing web app manifest, so the browser itself issues image/font/manifest Sec-Fetch-Dest requests to unknown paths — no header spoofing needed.

For each of missing-icon.png, missing-font.woff2, site.webmanifest: response status 404, content-type: text/plain, body Not Found, cache-control: private, no-cache, no-store, max-age=0, must-revalidate (matching the header Next.js itself sets for this response — see router-server.ts in vercel/next.js).

Then navigate to /does-not-exist directly (document request) — still renders the custom not-found page as text/html, unaffected.

@changeset-bot

changeset-bot Bot commented Jul 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 40807f6

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

This PR includes changesets to release 1 package
Name Type
@next-community/adapter-vercel 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

@vercel

vercel Bot commented Jul 29, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
adapter-vercel-adapter Ready Ready Preview Jul 29, 2026 8:17pm

gaearon added 4 commits July 29, 2026 20:41
Sec-Fetch-Dest lets us tell that a request for e.g. a missing
web-app-manifest icon can never render HTML, so route it to the shared
plain text not-found asset in the error phase instead of paying for the
app's not-found output.
This repo intentionally has no local test setup (see #2) in favor of
relying on the main vercel/next.js repo's test suite against real
deployments. A vitest-based unit test reintroduces exactly the local
setup that was removed; coverage for this route instead comes from
next.js e2e tests run in adapter deploy mode.
Drifting from the vercel/next.js list isn't a correctness problem, just a
missed optimization, so say that instead of "keep in sync".
Keeps this route consistent with the /_next/static/ 404 route fixed on
main, instead of duplicating the header object with a stale copy that's
missing cache-control.
@gaearon
gaearon force-pushed the gaearon-not-found branch from 5fbd9a3 to 40807f6 Compare July 29, 2026 20:17
@gaearon
gaearon changed the base branch from main to fix-not-found-txt-cache-control July 29, 2026 20:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant