Skip to content

[remix] Prevent 404 responses from being cached with immutable headers#14828

Merged
jeffsee55 merged 3 commits intomainfrom
jeffsee55/fix-remix-404-cache
Feb 2, 2026
Merged

[remix] Prevent 404 responses from being cached with immutable headers#14828
jeffsee55 merged 3 commits intomainfrom
jeffsee55/fix-remix-404-cache

Conversation

@jeffsee55
Copy link
Contributor

@jeffsee55 jeffsee55 commented Feb 2, 2026

Summary

  • Move cache-control headers for static assets from before handle: 'filesystem' to after handle: 'hit'
  • This ensures immutable cache headers are only applied when a file actually exists
  • Prevents 404 responses from being cached for 1 year with immutable headers

Background

When cache-control headers were placed BEFORE handle: 'filesystem', the headers would be applied to all matching requests, including 404s. This caused 404 responses for non-existent assets to be cached for 1 year with immutable headers, making them impossible to fix without cache purging.

The correct pattern (used by Next.js) is to place cache headers AFTER handle: 'hit', which only applies them when a file is found:

// routes to call after a file has been matched
{ handle: 'hit' },
// Before we handle static files we need to set proper caching headers
{
  src: '/_next/static/...',
  headers: { 'cache-control': 'public,max-age=31536000,immutable' },
  ...
}

See: https://github.com/vercel/vercel/blob/main/packages/next/src/server-build.ts#L2748-L2766

Test plan

  • Added integration test probe to verify 404 responses don't get immutable cache headers
  • Similar test assertion pattern used here:
    "path": "/_next/static/invalid-build-id/pages/non-existent.js",
    "notResponseHeaders": {
    "cache-control": "public,max-age=31536000,immutable"
    }

Fixes: https://linear.app/vercel/issue/BE-415

Manual curl of test fixture vs older behavior

➜  vercel-2 git:(main) curl -I https://01-no-preset-g45ffbb2a-zero-conf-vtest314.vercel.app/assets/does-not-exist-abc123.js
HTTP/2 404 
age: 0
cache-control: public, max-age=0, must-revalidate
content-type: text/html
date: Mon, 02 Feb 2026 17:38:53 GMT
server: Vercel
strict-transport-security: max-age=63072000; includeSubDomains; preload
x-robots-tag: noindex
x-vercel-cache: MISS
x-vercel-id: sfo1::iad1::b2sp6-1770053933572-302fafb09751

➜  vercel-2 git:(main) curl -I https://01-no-preset-hw782p5gz-zero-conf-vtest314.vercel.app/assets/does-not-exist-abc123.js
HTTP/2 404 
age: 0
cache-control: public, max-age=31536000, immutable
content-type: text/html
date: Mon, 02 Feb 2026 17:39:04 GMT
server: Vercel
strict-transport-security: max-age=63072000; includeSubDomains; preload
x-robots-tag: noindex
x-vercel-cache: MISS
x-vercel-id: sfo1::iad1::cf57s-1770053944387-bb701ce00343

🤖 Generated with Claude Code

Move cache-control headers for static assets from before `handle: 'filesystem'`
to after `handle: 'hit'`. This ensures the immutable cache headers are only
applied when a file actually exists, preventing 404 responses from being
cached for 1 year.

This follows the same pattern used by the Next.js builder, which places
cache headers in the 'hit' phase to avoid caching 404s.

Fixes: https://linear.app/vercel/issue/BE-415

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@jeffsee55 jeffsee55 requested a review from a team as a code owner February 2, 2026 17:01
@changeset-bot
Copy link

changeset-bot bot commented Feb 2, 2026

🦋 Changeset detected

Latest commit: 019a869

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

This PR includes changesets to release 2 packages
Name Type
@vercel/remix-builder Patch
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

@github-actions
Copy link
Contributor

github-actions bot commented Feb 2, 2026

📦 CLI Tarball Ready

The Vercel CLI tarball for this PR is now available!

Quick Test

You can test this PR's CLI directly by running:

npx https://vercel-k2o2ouq7k.vercel.sh/tarballs/vercel.tgz --help

Use in vercel.json

To use this CLI version in your project builds, add to your vercel.json:

{
  "build": {
    "env": {
      "VERCEL_CLI_VERSION": "vercel@https://vercel-k2o2ouq7k.vercel.sh/tarballs/vercel.tgz"
    }
  }
}

@github-actions
Copy link
Contributor

github-actions bot commented Feb 2, 2026

🧪 Unit Test Strategy

Comparing: a909ba1019a869 (view diff)

Strategy: Affected packages only

✅ Only testing packages that have been modified or depend on modified packages.

Affected packages - 2 (5%)
  1. @vercel/remix-builder
  2. vercel
Unaffected packages - 39 (95%)
  1. @vercel-internals/get-package-json
  2. @vercel/backends
  3. @vercel/build-utils
  4. @vercel/cervel
  5. @vercel/cli-auth
  6. @vercel/client
  7. @vercel/config
  8. @vercel/detect-agent
  9. @vercel/edge
  10. @vercel/elysia
  11. @vercel/error-utils
  12. @vercel/express
  13. @vercel/fastify
  14. @vercel/firewall
  15. @vercel/frameworks
  16. @vercel/fs-detectors
  17. @vercel/functions
  18. @vercel/gatsby-plugin-vercel-builder
  19. @vercel/go
  20. @vercel/h3
  21. @vercel/hono
  22. @vercel/hydrogen
  23. @vercel/introspection
  24. @vercel/koa
  25. @vercel/nestjs
  26. @vercel/next
  27. @vercel/node
  28. @vercel/oidc
  29. @vercel/oidc-aws-credentials-provider
  30. @vercel/python
  31. @vercel/python-analysis
  32. @vercel/redwood
  33. @vercel/related-projects
  34. @vercel/routing-utils
  35. @vercel/ruby
  36. @vercel/rust
  37. @vercel/static-build
  38. @vercel/static-config
  39. examples

Results

  • Unit tests: Only affected packages will run unit tests
  • E2E tests: Handled separately (Version Packages PRs or run-e2e-tests label)
  • Type checks: Only affected packages will run type checks

This comment is automatically generated based on the affected testing strategy

Prevent 404 responses from being cached by adding immutable headers.
Copy link
Member

@tknickman tknickman left a comment

Choose a reason for hiding this comment

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

Can we add a test as well?

@jeffsee55
Copy link
Contributor Author

jeffsee55 commented Feb 2, 2026

Can we add a test as well?

I added a probe to assert that we don't put that cache-control header on the response. Add a unit test as well you think?

@jeffsee55 jeffsee55 merged commit c3104a1 into main Feb 2, 2026
242 of 244 checks passed
@jeffsee55 jeffsee55 deleted the jeffsee55/fix-remix-404-cache branch February 2, 2026 20:26
jeffsee55 pushed a commit that referenced this pull request Feb 3, 2026
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## vercel@50.10.0

### Minor Changes

- feat(cli): Add webhooks command for managing webhooks
([#14789](#14789))

Adds a new `webhooks` command to the Vercel CLI with the following
subcommands:

- `webhooks ls` - List all webhooks with optional `--format json` output
- `webhooks get <id>` - Get details of a specific webhook with optional
`--format json` output
- `webhooks create <url> --event <event>` - Create a new webhook with
specified events
- `webhooks rm <id>` - Remove a webhook with `--yes` flag to skip
confirmation

Webhook event types are fetched dynamically from the OpenAPI spec to
stay in sync with the API.

- Added experimental services support in the CLI new project flow. When
`VERCEL_USE_EXPERIMENTAL_SERVICES=1` is set and a project's
`vercel.json` contains `experimentalServices`, the CLI will detect and
display the configured services during project setup, automatically
selecting the "services" framework preset.
([#14776](#14776))

### Patch Changes

- Improve logsv2 command output format with compact single-line display,
text-based level labels, dynamic column widths, and smart date display
([#14767](#14767))

- Skip update check when running on Vercel to prevent unnecessary worker
spawning in build container
([#14794](#14794))

- fix --help exit codes for marketplace commands
([#14834](#14834))

- Updated dependencies
\[[`687f73cebb6ae1cdd7c7feb0910967de99a17ad6`](687f73c),
[`e7c5d5fd41e124ef7314978b351696d130e89917`](e7c5d5f),
[`c3104a1ae9dbf9048e08bb2fa85605a95b254876`](c3104a1),
[`b029736b4be8dac135bef77283f47e1450faf0a9`](b029736),
[`d36c1ad3ddaf9303041e61a0a41d973b02007988`](d36c1ad),
[`5b31b133970539986ff9e98013d2c364536bd0b5`](5b31b13)]:
    -   @vercel/next@4.15.21
    -   @vercel/python@6.5.0
    -   @vercel/remix-builder@5.5.10
    -   @vercel/backends@0.0.25
    -   @vercel/static-build@2.8.28
    -   @vercel/node@5.5.28
    -   @vercel/express@0.1.36

## @vercel/python@6.5.0

### Minor Changes

- vendor Python runtime dependencies
([#14827](#14827))

- Bump vercel-runtime version automatically on its releases
([#14842](#14842))

## @vercel/python-analysis@0.3.0

### Minor Changes

- initial implementation of Python semantic analysis in Rust
([#14690](#14690))

## @vercel/backends@0.0.25

### Patch Changes

- Improve handling of cjs/esm interop during imports
([#14798](#14798))

## @vercel/cervel@0.0.12

### Patch Changes

- Improve handling of cjs/esm interop during imports
([#14798](#14798))

- Updated dependencies
\[[`d36c1ad3ddaf9303041e61a0a41d973b02007988`](d36c1ad)]:
    -   @vercel/backends@0.0.25

## @vercel/express@0.1.36

### Patch Changes

- Updated dependencies
\[[`d36c1ad3ddaf9303041e61a0a41d973b02007988`](d36c1ad)]:
    -   @vercel/cervel@0.0.12
    -   @vercel/node@5.5.28

## @vercel/fs-detectors@5.7.20

### Patch Changes

- Added experimental services support in the CLI new project flow. When
`VERCEL_USE_EXPERIMENTAL_SERVICES=1` is set and a project's
`vercel.json` contains `experimentalServices`, the CLI will detect and
display the configured services during project setup, automatically
selecting the "services" framework preset.
([#14776](#14776))

## @vercel/functions@3.4.1

### Patch Changes

- Fix InMemoryCache to use JSON serialization for consistency with
RuntimeCache ([#14751](#14751))

InMemoryCache now serializes values with `JSON.stringify()` on set and
deserializes with `JSON.parse()` on get, matching the behavior of
RuntimeCache. This ensures consistent behavior when switching between
cache implementations (e.g., in-memory for development, remote for
production), particularly for types that don't survive JSON round-trips
like `Date`, `Map`, `Set`, and `undefined`.

## @vercel/introspection@0.0.11

### Patch Changes

- Updated dependencies
\[[`d36c1ad3ddaf9303041e61a0a41d973b02007988`](d36c1ad)]:
    -   @vercel/backends@0.0.25

## @vercel/next@4.15.21

### Patch Changes

- Strip routes-manifest.json for determinism
([#14783](#14783))

- Update Next.js adapter version
([#14801](#14801))

## @vercel/remix-builder@5.5.10

### Patch Changes

- [remix] Prevent 404 responses from being cached with immutable headers
([#14828](#14828))

## @vercel/python-runtime@0.3.0

### Minor Changes

- vendor Python runtime dependencies
([#14827](#14827))

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants