#2599·wxt

Dev mode produces cross-origin worker URLs — `new Worker()` crashes the render process in Chrome 148+

Author: PurpleTapeCreated Aug 25, 2026Updated Aug 25, 2026
Labelspending-triage

Describe the bug

In dev mode, WXT serves extension page scripts from the Vite dev server, so Vite's URL helpers resolve against http://localhost:3000 while the page's own origin is chrome-extension://<id>. As a result, import workerUrl from './worker?worker&url' inside a sidepanel/popup/options entrypoint yields a cross-origin URL.

Per the HTML spec, dedicated workers must be same-origin — new Worker() with a cross-origin URL is required to throw a SecurityError DOMException. Chrome 147 and earlier didn't enforce this and the worker simply ran, so the problem went unnoticed. Starting with Chrome 148 the restriction is enforced — but instead of throwing, the render process is terminated (EXC_BREAKPOINT / SIGTRAP, consistent with an internal CHECK). The user sees the "extension has crashed, click to reload" notification and has to reload from chrome://extensions.

Filed against Chromium here: https://issues.chromium.org/issues/532577557

Chrome enforcing same-origin is correct — the crash-instead-of-throw is their bug. But it also means WXT dev mode currently generates worker URLs that are invalid per spec, and on 148+ that surfaces as a hard crash rather than a catchable error.

Applies to ?worker as well as ?worker&url — the generated WorkerWrapper calls new Worker() with the same localhost URL.

Expected: worker URLs on extension pages resolve to the extension origin in dev mode as well as production, so new Worker() is same-origin.

Workaround — patch Worker in dev to wrap cross-origin URLs in a same-origin Blob shim. The Blob URL inherits the page origin, and the import inside the worker is a plain module fetch, which is CORS-governed rather than origin-governed, so the dev server can still serve the actual module:

typescript
if (import.meta.env.DEV) {
    const Native = Worker;

    class PatchedWorker extends Native {
        constructor(scriptUrl: string | URL, options?: WorkerOptions) {
            const url = String(scriptUrl);
            if (url.startsWith('http://localhost') || url.startsWith('http://127.0.0.1')) {
                const shim = `import ${JSON.stringify(url)};`;
                const blob = new Blob([shim], { type: 'text/javascript' });
                super(URL.createObjectURL(blob), { ...options, type: 'module' });
            } else {
                super(scriptUrl, options);
            }
        }
    }

    (globalThis as Record<string, unknown>).Worker = PatchedWorker;
    console.log('PATCHED');
}

Import before creating any worker. The block is stripped from production builds via import.meta.env.DEV. Presumably there's a cleaner fix at the framework level.

Thanks for WXT — it's a pleasure to build extensions with. Happy to test any fix.

Reproduction

https://github.com/PurpleTape/wxt-test

Steps to reproduce

  1. npm install
  2. npm run dev
  3. Click the extension action to open the side panel

The sidepanel logs a http://localhost:3000/... worker URL and calls new Worker() → the extension's render process crashes and must be reloaded from chrome://extensions.

Note the background entrypoint in the repro resolves the same import to a chrome-extension://.../assets/ URL, which is same-origin and does not crash — the only difference is the origin of the URL, not the import syntax.

Requires Chrome 148 or newer. On 147 and earlier the worker is created and runs normally.

System Info

bash
System:
    OS: macOS 26.5.2
    CPU: (12) arm64 Apple M2 Max
    Memory: 1.14 GB / 96.00 GB
    Shell: 5.3.9 - /opt/homebrew/bin/bash
  Binaries:
    Node: 26.4.0 - /opt/homebrew/bin/node
    npm: 11.17.0 - /opt/homebrew/bin/npm
    pnpm: 11.3.0 - /opt/homebrew/bin/pnpm
  Browsers:
    Chrome: 151.0.7922.174
    Firefox: 153.0.4
    Safari: 26.5.2
  npmPackages:
    wxt: ^0.20.25 => 0.20.27

Used Package Manager

npm

Validations