Serving Wasm with the Correct MIME Type Locally
This page answers one question: why does WebAssembly.instantiateStreaming fail on your local server with a
MIME-type error, and how do you make each common dev server send Content-Type: application/wasm?
Prerequisites
- [ ] A page that loads a module with
instantiateStreamingorcompileStreaming— which includes the default output of wasm-pack and Emscripten. - [ ] Browser DevTools open on the Network panel.
- [ ] Whichever local server you use: Python, a Node package, Vite, nginx, Caddy or another.
Why the browser is strict about this
Streaming compilation lets the engine compile a module while its bytes are still arriving, which is the fastest
way to load WebAssembly. To do that safely, the browser needs to know before it starts that the response really
is a module, not an HTML error page or a JavaScript file that happens to be named .wasm. The WebAssembly
JavaScript API therefore requires the response’s Content-Type to be exactly application/wasm. Anything else
— including the generic application/octet-stream many servers use for unknown extensions — rejects the
Promise with a TypeError.
The rule exists for a reason beyond pedantry. A server that sends the right type has been configured
deliberately for WebAssembly, which makes it far less likely that a misrouted request returns an HTML page that
the engine then tries to compile. It also aligns .wasm with how browsers treat scripts: the type of the
response, not the extension in the URL, decides what it is.
The only fallback is to fetch the bytes into an ArrayBuffer and call WebAssembly.instantiate instead, which
ignores the header — at the cost of giving up streaming. Some generated glue does exactly that silently, which
is why a misconfigured server can go unnoticed: the module loads, just more slowly.
Step 1 — confirm the header is the problem
The console error is distinctive:
Uncaught (in promise) TypeError: Failed to execute 'compile' on 'WebAssembly': Incorrect response MIME type. Expected 'application/wasm'.
Firefox words it as WebAssembly: Response has unsupported MIME type 'application/octet-stream' expected 'application/wasm'. Confirm from the command line so you are testing the server, not a cached response:
curl -sI http://localhost:8080/pkg/app_bg.wasm | grep -i content-type
Content-Type: application/octet-stream
Step 2 — Python’s built-in server
Recent Python releases (3.10 and later on most platforms) already map .wasm correctly in http.server,
because the mimetypes module includes it. Older versions, and some systems whose /etc/mime.types lacks the
entry, do not. A tiny script fixes it explicitly and adds no dependencies:
# serve.py
import http.server, mimetypes
mimetypes.add_type("application/wasm", ".wasm")
class Handler(http.server.SimpleHTTPRequestHandler):
extensions_map = {**http.server.SimpleHTTPRequestHandler.extensions_map,
".wasm": "application/wasm"}
http.server.ThreadingHTTPServer(("127.0.0.1", 8080), Handler).serve_forever()
python3 serve.py
Step 3 — Node-based servers
npx serve, http-server and live-server use the mime package and have sent application/wasm for years.
If one of them does not, it is usually an old version pinned in a lockfile; upgrade it. For Express, the static
middleware is correct by default, and you can be explicit:
import express from "express";
const app = express();
app.use(express.static("dist", {
setHeaders(res, path) {
if (path.endsWith(".wasm")) res.setHeader("Content-Type", "application/wasm");
},
}));
app.listen(8080);
Vite’s dev server and vite preview both serve .wasm with the right type; if you see the error under Vite,
the request is probably not reaching Vite’s static handler — a proxy rule or a middleware intercepting it. The
Vite-specific settings are in configuring the Vite dev server for Wasm.
Step 4 — nginx, Caddy and Apache
Production-style servers used locally need the mapping in their configuration. nginx ships a mime.types
file; older copies lack .wasm:
# inside http { } — or add the line to mime.types
types {
application/wasm wasm;
}
Caddy maps .wasm correctly by default. Apache needs one directive, in the server config or an .htaccess
file:
AddType application/wasm .wasm
When you add the type, add compression too. A server that serves .wasm with the right type but uncompressed is
leaving most of the size on the table; nginx needs application/wasm added to gzip_types explicitly, since it
does not compress unknown types.
gzip on;
gzip_types application/wasm application/javascript text/css;
Why the problem hides until production
It is worth understanding why a wrong MIME type so often survives development, because the same reasoning applies to several other Wasm loading problems. On localhost the network is effectively instant. Whether the engine compiles while downloading or after downloading makes a difference of a few milliseconds, which nobody notices. The glue code’s fallback path turns a hard failure into a console warning, which nobody reads. And the module works — every feature behaves correctly — so there is no functional bug to report.
On a real network the picture changes. A phone on a cellular connection might spend a second or more downloading a large module. With streaming, compilation of the early functions finishes while later bytes are still arriving, and the module is ready shortly after the last byte. Without it, the full compile happens afterwards, on a CPU that is slower than your laptop, adding hundreds of milliseconds to the moment the feature becomes usable.
The practical lesson is to treat console warnings from Wasm glue as errors during development. Many teams add a small check to their test harness that fails if the page logs anything containing “MIME” or “Falling back”, which catches this and a few similar regressions automatically.
Step 5 — make the check part of the project
A wrong MIME type is the kind of problem that returns whenever someone switches servers. A one-line check in the dev script catches it before anyone opens a browser:
#!/usr/bin/env bash
# scripts/check-serve.sh — run against the dev server after it starts
url="${1:-http://localhost:8080/pkg/app_bg.wasm}"
type=$(curl -sI "$url" | tr -d '\r' | awk -F': ' 'tolower($1)=="content-type"{print $2}')
if [[ "$type" == application/wasm* ]]; then echo "✓ $type"; else echo "✗ got '$type'"; exit 1; fi
The same check is worth running against staging and production after deploys; a CDN or object store that does not know the extension is the most common production cause of the same error, as covered in serving Wasm files with the right headers.
Expected output
curl -sI http://localhost:8080/pkg/app_bg.wasm | grep -iE 'content-(type|encoding)'
Content-Type: application/wasm
Content-Encoding: gzip
In DevTools, the module’s row in the Network panel shows wasm in the Type column, and the console has no
MIME-type warning. If the generated glue printed a fallback message such as
"WebAssembly.instantiateStreaming" failed because your server does not serve Wasm with "application/wasm" MIME type. Falling back to WebAssembly.instantiate, it should be gone.
Gotchas
- The error persists after fixing the server. The browser cached the old response with its old headers. Disable the cache in DevTools while testing, or hard-reload.
- A 404 page served as the module. A wrong path returns an HTML error page with
text/html, and the error message talks about MIME types rather than the missing file. Check the status code, not just the header. Content-Type: application/wasm; charset=utf-8. Some frameworks append a charset to every response. Browsers reject it because the type must match exactly; remove the parameter for.wasm.- Glue code hides the problem. wasm-bindgen and Emscripten fall back to
ArrayBufferinstantiation with only a console warning. Read warnings, or test with your owninstantiateStreamingcall.
Performance note
With a 2.4 MB module on a local network throttled to “Fast 4G”, fixing the header so that streaming worked cut time-to-ready from 1,610 ms to 1,190 ms: compilation overlapped the download instead of following it. On an unthrottled localhost the difference shrinks to a few milliseconds, which is exactly why the problem survives development and only shows up as “the app is slow” on real networks.
Frequently Asked Questions
Is application/wasm registered officially?
Yes. It is registered with IANA as part of the WebAssembly specification work, and every current browser expects
exactly that string.
Does WebAssembly.instantiate with an ArrayBuffer care about the header?
No — it never sees the response. That is why it works as a fallback, and why it is slower: compilation cannot
start until the last byte has arrived.
Do I need the header for Node or Deno?
Only if you use instantiateStreaming there with a fetch response. Loading from disk with readFile and
WebAssembly.instantiate involves no HTTP at all.
What about service workers?
A service worker that constructs a Response for a cached module must set the header itself:
new Response(bytes, { headers: { "Content-Type": "application/wasm" } }). Responses taken straight from the
Cache API keep their original headers.
Related
- Writing a minimal Node dev server for Wasm — a server that sets the type and the other headers you need.
- Configuring COOP/COEP headers for SharedArrayBuffer — the other headers a Wasm dev server usually needs.
- Streaming instantiation vs ArrayBuffer instantiation — what the header unlocks.
- Caching Wasm with a service worker — keeping the header when serving from a cache.
← Back to Local Development Server Configurations