A JavaScript MIME type error means the browser received a response it cannot accept as JavaScript. If a module request returns text/html, inspect the response body before changing headers: you may be loading the app shell, a login page, or an error document instead of the script. Fix the asset URL or routing rule that produced that response.
This guide covers the deployed-site failure behind a blank page or broken navigation: how to inspect the exact request, distinguish missing files from incorrect headers, repair the delivery path, and keep the failure out of the next release.
JavaScript MIME type error troubleshooting checklist
- Copy the failing script URL from the browser's Network panel.
- Record its status, redirects,
Content-Type, and response body. - Compare the requested path with the files in the deployed build.
- Keep missing asset requests out of the SPA HTML fallback.
- Correct the content type only when the body is actually JavaScript.
- Check root and nested routes, including a direct refresh.
- Compare a fresh browser with one that used the previous release.
- Add a deployed asset contract and a browser interaction test to CI.
The JavaScript console errors guide covers broader runtime failures. Here, start with the resource response: a successful document request says nothing about whether its scripts can execute.
Why a module script rejects text/html
An ES module script needs a JavaScript-compatible response MIME type. MDN's JavaScript modules guide explains strict MIME checking and recommends text/javascript. The filename extension and the HTML type="module" attribute do not change the server's response header.
A valid response for an existing module can look like this:
HTTP/2 200
Content-Type: text/javascript; charset=utf-8
X-Content-Type-Options: nosniff
text/html, text/plain, and application/octet-stream are not substitutes for a JavaScript module content type. MDN's MIME types reference describes JavaScript media types and the distinction between a MIME type and its optional parameters.
Keep X-Content-Type-Options: nosniff. It blocks script and style responses with incompatible MIME types in the relevant request contexts, as described in MDN's header reference. Removing it does not remove module scripts' strict checking. Repair the response rather than weakening the browser policy.
Do not change every response to text/javascript. HTML documents still need their own type, and an HTML error body mislabeled as JavaScript remains an error. Review the surrounding website security headers after the delivery fix.
Inspect the failing script response
Open DevTools before reloading. In Network, select the failing resource and inspect Headers, Response, and Initiator. Copy the complete URL, including the deployment prefix. Chrome's Network reference documents these views and the Disable cache control.
Classify what you actually received:
- HTML app shell with status 200: a fallback probably handled a missing asset or an incorrectly resolved path.
- HTML error document with status 404: check the build output, upload, and requested filename.
- Login page after a redirect: review the authentication rule applied to public assets.
- JavaScript body with a non-JavaScript type: inspect the static server's MIME mapping or a proxy header override.
- Older JavaScript filename: compare the document release, retained assets, CDN response, and service worker state.
These are diagnostic starting points, not proof of the owner. Check the actual hosting configuration before editing it.
Use a GET request to save the same public resource outside the browser. Replace this example URL with the one from Network:
asset_url='https://example.com/assets/app-a1b2c3.js'
response_dir=$(mktemp -d)
curl --silent --show-error --compressed \
--dump-header "$response_dir/headers.txt" \
--output "$response_dir/body.txt" \
"$asset_url"
cat "$response_dir/headers.txt"
head -c 300 "$response_dir/body.txt"
The curl manual documents these options. This command leaves redirects unfollowed so you can inspect Location before requesting the destination. Keep certificate verification enabled. If the request depends on cookies or other headers, compare that browser request separately; an unauthenticated curl response may differ.
A body beginning with an HTML document is enough to stop treating the issue as a JavaScript syntax bug. Save the response with the release ID and failing route so another developer can reproduce it.
Fix missing assets before changing MIME mappings
Compare the requested filename with the published artifact. If HTML references /assets/app-a1b2c3.js, that file must exist at that public path. Uploading source files, publishing the wrong directory, or omitting the assets directory can leave HTML available while its application code is missing.
Check the release artifact before the CDN:
- Find the script URL in the generated HTML or build manifest.
- Confirm the matching file exists in the output directory.
- Confirm that directory is the deployment's publish target.
- Request the uploaded asset at the public hostname.
- Compare the returned body with the expected artifact.
When the body really is JavaScript but the header is wrong, fix the component that sets Content-Type: the static file server, object metadata, edge response, or proxy override. Check .mjs as well as .js when your build emits both. Verify the delivered header again instead of assuming the source configuration reached production.
Exclude assets from SPA rewrites
A client-side route may need an HTML fallback on refresh. A missing script should not use that fallback. Give the asset namespace an explicit rule before the document routing rule.
For an Nginx server serving a root-hosted SPA, this illustrative excerpt separates /assets/ from application routes:
# Inside an existing server block; TLS is configured separately.
root /srv/site/dist;
include /etc/nginx/mime.types;
location ^~ /assets/ {
try_files $uri =404;
}
location / {
try_files $uri $uri/ /index.html;
}
Nginx's try_files documentation explains file checks and the final fallback. Adapt the root, asset namespace, and MIME include path to your installation. Include other generated asset directories when applicable and inspect competing locations. This excerpt does not implement accurate not-found responses for every application route; the soft 404 guide covers that separate contract.
For managed hosting, express the same order through its supported rewrite rules. Request one existing script and one invented filename under the asset prefix. The existing script should return JavaScript; the invented filename should return a genuine not-found status, never a successful app shell.
Correct deployment base paths and nested routes
If the site lives at https://example.com/project/, a root-relative /assets/app.js points outside /project/. A document-relative assets/app.js can also resolve differently when a visitor opens a nested route. Compare the browser's resolved URL with the intended public asset location.
For a Vite build hosted under /project/, configure the public base path:
import { defineConfig } from "vite";
export default defineConfig({
base: "/project/",
});
Vite's public base path documentation explains how it rewrites generated asset references. Rebuild and redeploy; changing configuration does not rewrite files already published. Review manually constructed URLs separately.
Test a homepage load, a direct nested-route load, a refresh there, and client-side navigation to a route that imports another chunk. A correct entry script can hide a broken lazy-loaded dependency until someone opens that section.
Check stale HTML and service worker responses
If only returning visitors fail, compare the script filename in their document with the current release. Old HTML or an open tab may request a chunk removed by a later deployment. Restoring the required retained asset can repair that path; relabeling the fallback HTML cannot.
Use the Cache-Control header guide to review document revalidation, fingerprinted assets, and deployment retention. Keep old and new release evidence separate rather than clearing caches before you capture the failure.
When a service worker controls the site, inspect it in Chrome's Application panel and compare with Bypass for network. Chrome's service worker debugging guide explains that control. If bypass changes the response, investigate the worker's cache and fallback rules as well as the server. Test the returning-user path again after the fix.
Add a JavaScript MIME type error check to CI
Use an exact asset path from the current build, not a guessed filename. This Playwright Test example adopts text/javascript as the site's explicit delivery contract and also checks that a missing asset cannot become HTML with status 200:
import { expect, test } from "@playwright/test";
test("release script has the expected response", async ({ request }) => {
const origin = process.env.RELEASE_ORIGIN;
const assetPath = process.env.RELEASE_SCRIPT_PATH;
if (!origin || !assetPath) {
throw new Error("Set RELEASE_ORIGIN and RELEASE_SCRIPT_PATH from this build");
}
const base = new URL(origin);
const asset = new URL(assetPath, base);
expect(asset.origin).toBe(base.origin);
const response = await request.get(asset.href, { maxRedirects: 0 });
expect(response.status()).toBe(200);
const mediaType = (response.headers()["content-type"] ?? "")
.split(";")[0].trim().toLowerCase();
expect(mediaType).toBe("text/javascript");
expect(response.headers()["x-content-type-options"]).toBe("nosniff");
expect((await response.body()).length).toBeGreaterThan(0);
const missingUrl = new URL("__mime-probe-missing-7f31c9.js", asset);
const missing = await request.get(missingUrl.href, { maxRedirects: 0 });
expect(missing.status()).toBe(404);
});
Supply an asset path including any deployment prefix, and reserve the probe filename so it cannot collide with a real file. If your approved server uses another valid JavaScript MIME type, change the exact expectation deliberately. This test assumes same-origin assets; give an intentional asset CDN its own explicit host contract.
Playwright's APIRequestContext reference documents request and redirect handling. The test checks delivery, not execution or all imported modules. Pair it with a browser test that loads a nested route, asserts an application-ready state, and opens a lazy-loaded feature. Use Playwright response events when inspecting script statuses: an HTTP error response is different from a transport failure.
Retain the failing URL, status, media type, build revision, and browser result using the website regression testing workflow. Run against the public release too if preview hosting uses different routing rules.
Verify the repaired public page with Slop Audit
After the asset and browser tests pass, run Slop Audit on the repaired public URL. Review the observed same-site console/page errors, failed network requests, and the audited document's X-Content-Type-Options result. Use the evidence to identify remaining public-page failures, then rescan after the next repair.
Slop Audit gathers limited desktop and mobile browser evidence when the browser stage is available. It does not inventory every script MIME type, exercise every lazy-loaded route, or validate service worker upgrade behavior. A document-level nosniff pass does not prove asset headers are correct, and an HTTP 200 HTML fallback can escape a failed-request check. Keep the focused delivery test and the real user path beside the audit report when signing off the release.