Why My VS Code Webview Workers Were Returning 401
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 Approach | Origin Context | Webview Auth Headers | Production Behavior |
|---|---|---|---|
Relay Blob (import "...uri") | blob: origin | Missing (Fails credential check) | 401 Unauthorized Crash |
Content Blob (new Blob([src])) | blob: self-contained | Not 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
- VS Code webview auth tokens are bound exclusively to main-thread document requests.
- When the worker executes
import "vscode-resource://...", the request originates from theblob:origin context. - 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
Patchwindow.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-threadfetch() 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.