Engineering Journal
Ginexys
Ginexys

Why My VS Code Webview Workers Were Returning 401

2026-05-30

TLDR

VS Code webview panels automatically attach authentication tokens to <script> and <link> tags requesting vscode-resource:// URIs, but omit auth headers for requests made inside blob-spawned Web Workers. Attempting to spawn workers via "relay blobs" (import "vscode-resource://...") returns HTTP 401 Unauthorized errors and crashes workers. The fix requires fetching worker source on the main thread (where auth works), wrapping bytes in self-contained content blobs, pre-fetching worker blobs at page init, and adding ${panel.webview.cspSource} to connect-src.
Worker Spawning ApproachOrigin ContextWebview Auth HeadersProduction Behavior
Relay Blob (import "...uri")blob: originMissing (Fails credential check)401 Unauthorized Crash
Content Blob (new Blob([src]))blob: self-containedNot Required (Bytes inlined)100% Reliable Execution

Blob-spawned Web Workers crash with HTTP 401 errors

When loading web tools (like Monaco Editor or PDF.js) inside VS Code webviews, console logs frequently display three cascading errors:

Could not create web worker(s). Falling back to main thread. pdf.worker.mjs: Failed to load resource: 401 () Error loading PDF: Geometry worker crashed: [object Event]

Monaco workers fail, PDF.js falls back to slow fake-worker mode, and background extraction workers crash with non-descript [object Event] exceptions.


Sandboxed origins strip authentication tokens from relay requests

Developers often spawn webview workers using "relay blobs", a thin blob worker that dynamically imports the target script URI:

// BROKEN: Relay Blob Spawning Pattern
const relayBlob = new Blob(
  [import "${workerVsCodeUri}";],
  { type: 'application/javascript' }
);
const blobUrl = URL.createObjectURL(relayBlob);
new Worker(blobUrl, { type: 'module' });

Why relay blobs return 401 errors

  1. VS Code webview auth tokens are bound exclusively to main-thread document requests.
  2. When the worker executes import "vscode-resource://...", the request originates from the blob: origin context.
  3. Because blob: contexts lack VS Code webview session credentials, the HTTP request hits VS Code's internal file server without headers, returning HTTP 401 Unauthorized.
Main Thread (Auth OK)           Web Worker (blob: origin)       VS Code File Server
        |                                   |                            |
        |=== [Broken Relay Blob Flow] ===================================|
        |                                   |                            |
        |--- 1. Spawn Worker(relayUrl) ---->|                            |
        |                                   |--- 2. import "vscode..." ->| (No Auth Headers!)
        |                                   |<-- 3. HTTP 401 ------------| (Worker Crashes)
        |                                   |                            |
        |=== [Production Content Blob Flow] =============================|
        |                                   |                            |
        |--- 4. fetch(vscodeResourceUri) ------------------------------->| (Auth Headers OK)
        |<-- 5. 200 OK + Raw Worker JS ----------------------------------|
        |--- 6. Create contentBlobUrl ------|                            |
        |--- 7. Spawn Worker(contentUrl) -->|                            | (Inlined Raw Bytes)

Pre-fetching worker source code resolves credential gaps

Fetching worker sources on the main thread preserves credentials

Fetch the raw worker source code on the main thread (where auth works) and wrap the raw bytes in a self-contained content blob:
// Main thread helper: Fetch source text with full webview auth
async function fetchContentBlob(workerUri) {
  const response = await fetch(workerUri); // Auth headers attached natively!
  const sourceText = await response.text();
  return URL.createObjectURL(
    new Blob([sourceText], { type: 'application/javascript' })
  );
}

// Pre-fetch worker blobs on page initialization window.__WORKER_BLOBS__ = {};

Promise.all(workerUris.map(async (uri) => { const blobUrl = await fetchContentBlob(uri); window.__WORKER_BLOBS__[uri] = blobUrl; })).then(() => { console.log('[GX] All worker blobs pre-fetched and ready.'); });

Patched Worker constructors resolve pre-fetched local blobs

Patch window.Worker to return pre-fetched content blobs synchronously:
const NativeWorker = window.Worker;

window.Worker = function PatchedWorker(scriptUrl, options) { const resolvedPath = getPathname(scriptUrl); const blobUrl = window.__WORKER_BLOBS__?.[resolvedPath];

if (blobUrl) { return new NativeWorker(blobUrl, options); }

console.warn('[GX] Worker blob not pre-fetched, falling back:', scriptUrl); return new NativeWorker(scriptUrl, options); };

Adding cspSource to connect-src permits local resource fetches

Ensure your Content Security Policy permits main-thread fetch() calls to vscode-resource:// URIs:
// Extension Host CSP Meta Tag Generation
const csp = 
  default-src 'none';
  script-src 'nonce-${nonce}' ${webview.cspSource};
  connect-src ${webview.cspSource} https://ginexys.com;
;

Directory scanning resolves hashed build artifacts

Bundlers like Vite append content hashes to worker outputs (for example, pdf.worker-BgryrOlp.mjs). Scan the assets/ directory at extension activation time and register both stable and hashed keys in your worker map:
const assetFiles = fs.readdirSync(path.join(assetDir, 'assets'));
const hashedPdfWorker = assetFiles.find(f => /^pdf\.worker-[A-Za-z0-9_-]+\.mjs$/.test(f));

const WORKER_MAP = { "/assets/pdf.worker.mjs": pdfWorkerUri, ...(hashedPdfWorker ? { [/assets/${hashedPdfWorker}]: pdfWorkerUri } : {}) };

Rule of thumb: Never spawn webview Web Workers via URL imports or relay blobs. Fetch worker source code on the main thread, construct self-contained content blobs, and pre-fetch worker blobs at page init.
Read this post in the full Engineering Journal →