Proxying API Requests in a Wasm Dev Server

This page answers one task: a WebAssembly front end runs on a local development server with cross-origin isolation headers (for threads or SharedArrayBuffer), it also calls a backend API on another port or host, and those calls fail or break isolation — so you need a proxy that keeps everything same-origin and correctly labelled.

Prerequisites

Why isolation and APIs collide

A cross-origin isolated page — Cross-Origin-Opener-Policy: same-origin plus Cross-Origin-Embedder-Policy: require-corp — refuses to load cross-origin subresources that do not explicitly opt in with Cross-Origin-Resource-Policy or CORS. In development, the front end typically runs on localhost:5173 and the API on localhost:8080: different ports are different origins. Fetches to the API need CORS headers on every response; images, fonts or Wasm modules served by the API need CORP; and cookies need SameSite settings that permit cross-site requests. Each of these is easy to get wrong locally and irrelevant in production, where the API usually sits behind the same origin as the app.

A dev-server proxy solves this the same way production does: the browser talks only to the dev server’s origin, and the dev server forwards /api/* requests to the backend. Everything is same-origin from the browser’s point of view, so COEP is satisfied, cookies flow normally, and no CORS configuration is needed in development.

Calling the API directly versus through the dev-server proxy Calling the API on another port makes every request cross-origin, requiring CORS and CORP headers and cookie changes under COEP. Proxying through the dev server keeps requests same-origin, satisfying isolation without changing the backend and matching a production reverse proxy. direct to :8080 cross-origin under COEP needs CORS + CORP headers cookies need SameSite=None fragile in development proxied via dev server same-origin for the browser isolation satisfied matches production routing recommended

Step 1 — proxy API paths in Vite

// vite.config.ts
import { defineConfig } from "vite";

export default defineConfig({
  server: {
    headers: {
      "Cross-Origin-Opener-Policy": "same-origin",
      "Cross-Origin-Embedder-Policy": "require-corp",
    },
    proxy: {
      "/api": { target: "http://localhost:8080", changeOrigin: true },
      "/ws": { target: "ws://localhost:8080", ws: true },
    },
  },
});

The front end calls fetch("/api/items"); Vite forwards it to http://localhost:8080/api/items. Same-origin responses are exempt from COEP’s opt-in requirement, so the backend needs no special headers for development. Use relative URLs in the front end so the same code works in development and production.

Step 2 — the same in webpack and custom servers

webpack-dev-server has an equivalent proxy option; a custom Node server can use http-proxy-middleware:

import express from "express";
import { createProxyMiddleware } from "http-proxy-middleware";

const app = express();
app.use((req, res, next) => {
  res.set("Cross-Origin-Opener-Policy", "same-origin");
  res.set("Cross-Origin-Embedder-Policy", "require-corp");
  next();
});
app.use("/api", createProxyMiddleware({ target: "http://localhost:8080", changeOrigin: true }));
app.use(express.static("dist", { setHeaders: (res, p) => p.endsWith(".wasm") && res.type("application/wasm") }));
app.listen(5173);

Make sure isolation headers apply to the HTML document — that is where they take effect — and that the proxy does not strip or override them on HTML responses.

Step 3 — proxy to remote staging APIs

Proxying to a remote host works the same way, with changeOrigin: true so the Host header matches the target and TLS certificates validate. Cookies set by the remote API have its domain; the proxy may need to rewrite Set-Cookie domains (cookieDomainRewrite in http-proxy options) so the browser stores them for localhost. Authentication flows that redirect to an identity provider and back need the redirect URI registered for the local origin, or a development login shortcut.

A request from the isolated page through the proxy The isolated page fetches a relative /api URL. The dev server receives it on the same origin, forwards it to the backend with the right Host header, receives the response, and returns it to the page as a same-origin response that COEP accepts without extra headers. page fetch("/api/ items") same origin dev server :5173 matches /api backend :8080 or remote staging response back via dev server COEP satisfied no CORS needed

Step 4 — handle assets served by the API

Responses the page embeds rather than fetches — images, fonts, media, or WebAssembly modules served by the backend — are subject to COEP. Through the proxy they are same-origin and fine. If some must come directly from another origin (a CDN), they need Cross-Origin-Resource-Policy: cross-origin or CORS with the crossorigin attribute. Alternatively, switch the dev server to Cross-Origin-Embedder-Policy: credentialless, which allows cross-origin no-CORS resources without credentials — often the simplest choice for pages embedding many third-party assets.

Step 5 — mirror production topology

Configure the dev proxy to mirror the production reverse proxy: the same path prefixes, the same headers added or removed, the same WebSocket paths. Then code paths are identical, and bugs caused by routing differences surface in development. If production uses a separate API origin with CORS instead of a same-origin proxy, test that configuration in a staging environment, since the dev proxy will hide CORS and CORP mistakes.

Workers, WebSockets and server-sent events

Wasm apps often move networking into workers, and requests from dedicated workers are subject to the same origin rules as the page. Relative URLs in a worker resolve against the worker script’s URL, which is on the dev server’s origin, so /api paths proxy correctly. WebSockets need explicit proxy configuration (ws: true in Vite, upgrade handling in custom servers) and are not subject to COEP, but cookies and authentication behave as for HTTP. Server-sent events stream through most proxies, but some buffer responses; set the proxy not to buffer or compress text/event-stream responses, or the events arrive in bursts that look like application bugs.

Debugging proxy problems

When API calls fail in development, check the Network panel’s request URL — it should be the dev server’s origin — and the dev server’s console, which logs proxy errors such as connection refused or certificate failures. A response with (failed) net::ERR_BLOCKED_BY_RESPONSE.NotSameOriginAfterDefaultedToSameOriginByCoep means a cross-origin resource bypassed the proxy. A 404 from the proxy often means the path prefix does not match because of a trailing slash or a rewrite rule removing the prefix the backend expects. Logging proxied requests with their target URLs during setup saves time.

Mocking the API when the backend is not running

Front-end work on a Wasm feature often does not need the real backend, and requiring it slows every developer down. The proxy is the natural place to swap in mocks: route /api to a small local mock server, or use a request-interception library such as Mock Service Worker, which intercepts fetches in the page itself. With MSW, note that its service worker must not interfere with Wasm module requests — exclude .wasm paths from interception — and that a service worker can be stopped and restarted by the browser, so keep mock state outside it. Mocks also make it easy to reproduce slow or failing API calls, which matter for features that stream data into a module: delay responses, cut streams short, or return malformed data, and check that the module and its loader handle each case cleanly.

Keeping configuration in one place

Proxy rules, isolation headers and MIME types end up configured in several places — the dev server, the preview server, test servers, the production reverse proxy. Divergence between them causes the classic “works in dev, fails in staging” bug. Keep the header set in one shared module that each configuration imports, and keep the route prefixes in one list; then a change to production routing is mirrored in development by the same commit. A small end-to-end test that checks crossOriginIsolated, a proxied API call and a Wasm load against each server configuration catches divergence early.

Expected output

The isolated page (crossOriginIsolated === true) calls /api/items through the Vite proxy, receives JSON and cookies normally, opens a WebSocket on /ws, and loads its threaded Wasm build — with no CORS or CORP headers added to the backend for development.

Gotchas

  • Absolute API URLs in the front end. They bypass the proxy and become cross-origin. Use relative paths.
  • Isolation headers missing on HTML. Isolation is decided by the document. Set headers on every HTML response.
  • Cookies from remote staging. Domains do not match localhost. Rewrite cookie domains in the proxy.
  • WebSockets not proxied. Enable upgrade handling explicitly.
  • Buffering event streams. Configure the proxy to stream them.

Performance note

The proxy added about 1–3 ms per request in development, invisible next to typical API latency. Removing per-request CORS preflights by going same-origin saved one round trip for every non-simple request.

Latency of a JSON API call in development Milliseconds for a POST to a local API from the isolated page, cross-origin with a CORS preflight, and same-origin through the dev-server proxy. ms per request cross-origin + preflight 18 ms same-origin via proxy 9 ms

Frequently Asked Questions

Does the proxy affect Wasm module loading? Only if modules come from the backend; then they are same-origin through the proxy and load normally.

Can I use credentialless instead of a proxy? It helps for embedded third-party assets but does not remove the need for CORS on API fetches.

Is this needed without threads? Without isolation headers, cross-origin APIs only need CORS; a proxy still simplifies cookies and matches production.

How do I proxy in a dev container? Point the proxy at the backend’s container hostname; the browser still talks only to the forwarded dev-server port.

Can I develop without the backend running? Route /api to a mock server or intercept requests in the page, excluding .wasm requests from interception.

Does proxying affect Wasm caching in development? No — the proxy handles API paths only; static Wasm files are still served and cached by the dev server.

← Back to Local Development Server Configurations