CSS and JavaScript 404 After Deploy: Keep Older Vite Pages Working
Older tabs can request files that a new release has removed. Separate a missing build artifact from a compatibility gap, then test both sides of the deployment.
Keep the failed asset URL before refreshing
When a stylesheet or JavaScript chunk returns 404 after a release, copy its exact URL and identify which document or running script requested it. A hard refresh may fix that visitor's page while erasing the evidence that older pages cannot survive the deployment.
There are two common branches. A new document may reference an asset that was never shipped to its server. Alternatively, a document or application instance from the previous release may request a file that the new release deleted. Both produce missing assets, but their fixes belong at different points in the delivery process.
Vite describes the older-client case in its production build guidance (opens in a new tab). Hashed filenames distinguish versions; they do not guarantee that the server continues to store every version a browser might request.
Distinguish an incomplete release from an older client
Compare a fresh private browser session with a tab opened before the deployment. Inspect the original request in Network, not just the current homepage source. Dynamic imports can be requested much later than the first document load.
| Observation | Investigate first |
|---|---|
| Fresh HTML references a file missing on every web instance | Build output, manifest and image packaging |
| Only older tabs request the missing filename | Retention of previous build assets |
| The same asset alternates between 200 and 404 | Inconsistent replicas, origins or release routing |
| JavaScript URL returns HTML with status 200 | Catch-all routing or an error-page rewrite |
In a Laravel integration, the build manifest and files it names must come from a compatible build. Copying a new manifest without its matching files, or running old application templates against unrelated assets, can create failures before any browser cache is involved. Vite's backend integration guide (opens in a new tab) explains the manifest's role.
Inspect status, content type and cache behavior together
Run a bounded GET from a machine that should reach the public asset. Substitute the exact failed URL; the filename below is only an example.
curl --silent --show-error --connect-timeout 3 --max-time 10 \
--dump-header - --output /dev/null \
https://example.com/build/assets/app-oldhash.js
The command displays headers without treating every HTTP status as a shell failure. Read the returned status explicitly. A 200 with text/html is not a valid repair for a missing JavaScript module. Serve asset misses as asset misses instead of routing them to the application's HTML fallback.
A local HTTP and Chromium fixture modeled a page referring to an older stylesheet and script. Removing those files produced 404 responses. Retaining their original bytes restored both 200 responses, the expected text color and the script's ready marker. This tested the compatibility mechanism, not a particular Vite bundler version or a real CDN configuration.
Publish immutable assets before activating their callers
A robust release process makes the new asset set available before the corresponding HTML or application version begins referring to it. During a rolling or blue-green deployment, verify that every web path a user can reach can serve the required files.
Retain previous hashed files for a defined compatibility window. Choose that window using actual client behavior: long-lived admin tabs, cached documents, service workers and rollback needs can extend it. Retaining only the immediately previous release may be too short if several deployments happen in one day.
Use a storage location or deployment artifact that supports coexistence rather than replacing one asset directory in place. A separate versioned asset store is one option. A carefully managed union of current and retained hashed assets is another. In either case, avoid changing the bytes behind an existing immutable filename.
Retention is not permission to keep everything forever. Apply an age or release policy, account for disk space and handle urgent security removals deliberately. Do not copy private source files, credentials or arbitrary build-directory contents into a public asset store.
Set HTML and asset caching policies for their different roles
An unchanged hashed asset can be cached for a long period. A document that chooses asset names needs a freshness policy that allows clients to discover the next release. As a starting point for an appropriate deployment:
Cache-Control: no-cache
Use that on HTML when revalidation is intended. For genuinely content-addressed public assets, a separate policy can be:
Cache-Control: public, max-age=31536000, immutable
These are policies for different responses, not two headers to paste onto every route. In particular, no-cache allows storage but requires validation before reuse; it does not mean no-store. Review browser, CDN and service-worker behavior together using the Cache-Control reference (opens in a new tab).
Revalidation helps future navigations. It does not replace already-running JavaScript in an old tab. Retaining compatible assets still matters for imports triggered after a release.
Treat import-error recovery as a fallback
Vite exposes a preload-error event for dynamic import failures. A controlled recovery flow may offer a refresh or reload once after preserving recoverable state. Avoid an unconditional reload loop, especially while the asset store is unavailable or the browser is offline.
Do not silently discard unsaved form input to hide a deployment defect. An explicit “A new version is available” action is often safer for a long-lived application than immediately replacing the page. This fallback also does not repair missing entry stylesheets or every possible resource failure.
Verify both sides of the release boundary
Before calling a release successful, check a fresh document and an older tab. In the older tab, trigger a route or action that imports code not yet loaded. Repeat the test while traffic can reach either release during the switch.
Inspect the actual resource responses and confirm that the interface works. A 200 homepage and a running container do not establish that the files needed by older clients remain available. After fixing retention, keep monitoring missing assets and browser errors so that the next cleanup policy does not quietly reintroduce the same failure.
Catch problems after your next deployment
Monitor availability, browser JavaScript errors and visual changes with DeployWatchers.