Skip to content

fix(runtime): capture blob worker roots before revocation#35128

Open
divybot wants to merge 5 commits into
mainfrom
orch/divybot-541
Open

fix(runtime): capture blob worker roots before revocation#35128
divybot wants to merge 5 commits into
mainfrom
orch/divybot-541

Conversation

@divybot

@divybot divybot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Captures blob/object URL worker root modules during new Worker(blobUrl) so immediate URL.revokeObjectURL(blobUrl) does not race with worker graph preparation or module loading. The captured blob is used only for the worker root; dependencies and loader hooks still delegate to the normal module loader.

Adds a deno run regression spec for repeatedly constructing module workers from blob URLs and revoking each URL synchronously.

Tests:

  • cargo fmt --check
  • cargo check -p deno_runtime
  • cargo build -p deno --bin deno
  • cargo build -p test_server
  • cargo test --test specs specs::worker::worker_blob_url_revoke
  • cargo build -p denort --bin denort
  • cargo test --test specs specs::compile::worker_blob_url_revoke

Closes #26142
Closes denoland/divybot#541

Co-Authored-By: Divy Srivastava <me@littledivy.com>
@littledivy littledivy requested a review from bartlomieju June 11, 2026 03:57
@bartlomieju

Copy link
Copy Markdown
Member

Confirmed the JS race fix works (the added spec test passes), but this introduces a TypeScript regression: TS blob module workers no longer transpile.

Repro (works on main, broken on this branch):

const code = `
const x: number = 42;
self.onmessage = (e: MessageEvent) => { self.postMessage(x); };
`;
const url = URL.createObjectURL(new Blob([code], { type: "application/typescript" }));
const w = new Worker(url, { type: "module" });
URL.revokeObjectURL(url);
w.onmessage = (e) => { console.log("got", e.data); w.terminate(); };
w.postMessage(0);

On main this prints got 42. With this PR:

error: Uncaught (in worker "") SyntaxError: Expected ',', got ':'
  |
3 | self.onmessage = (e: MessageEvent) => { self.postMessage(x); };
  |                    ~

Root cause: CapturedBlobModuleLoader::load serves the captured blob's raw bytes for the root, and module_type_from_media_and_requested_type maps MediaType::TypeScript to ModuleType::JavaScript with no transpilation. The normal CliModuleLoader::load (which the wrapper bypasses for the root) is what strips TS.

Note the blob is captured for every module blob worker, not only revoked ones, so this breaks all TS blob module workers — even without revokeObjectURL.

Suggested direction: for the captured-content root, route through the inner loader's transpilation rather than returning raw bytes (the content is already injected into the graph via prepare_loadfile_content_overrides, so the inner load can serve the transpiled output), or transpile in load_captured_blob. Either way, please add a TS-blob-worker case to the spec test — the current fixture is JS-only so it wouldn't catch this.

(Minor, separate: the PR description lists cargo test --test specs specs::compile::worker_blob_url_revoke, but no tests/specs/compile/... fixture is in the diff.)

Move the blob/object-URL worker root capture from a runtime module-loader
wrapper into the CLI module loaders. The previous wrapper served the
captured blob's raw bytes for the root, which dropped the blob's media
type and broke transpilation of TypeScript blob module workers.

The capture is still taken synchronously at worker construction (so a
racing URL.revokeObjectURL can't break the load), but now the captured
content AND its content-type are injected into the worker's module graph
(deno run) / preferred over the revoked object-URL lookup (compile), so
the normal load path handles charset decoding and TS/JSX transpilation.

Adds a TypeScript case to the worker_blob_url_revoke spec.
@divybot

divybot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @bartlomieju — good catch. You were right that the root cause was serving the captured blob's raw bytes for the root, which dropped the media type so MediaType defaulted to JavaScript (the blob URL has no extension and file_content_overrides alone carries no content-type).

Fixed by moving the capture out of the runtime module-loader wrapper and into the CLI module loaders, where the media type can be preserved:

  • deno run (cli/module_loader.rs): the captured root blob is injected during prepare_load as both a file_content_overrides entry (raw bytes) and a file_header_overrides entry (content-type: <blob.media_type>). This mirrors exactly what the normal blob fetch (BlobStoreAdapter) returns, so the regular load path does charset decoding and TS/JSX transpilation. load just delegates to the inner loader, which serves the transpiled output from the graph. Added file_header_overrides to PrepareModuleLoadOptions for this.
  • compile (cli/rt/run.rs): EmbeddedModuleLoader's blob branch already transpiles via transpile_runtime_module; it now prefers the synchronously-captured root blob and falls back to get_object_url, so the revoke race is closed there too without changing the transpilation behavior.

The runtime-side CapturedBlobModuleLoader (and the now-unneeded deno_media_type dep on deno_runtime) are gone; the only remaining runtime change is the synchronous capture in op_create_worker.

Tests: added a TypeScript case to tests/specs/worker/worker_blob_url_revoke (ts.ts) — it loops 10 TS blob module workers with synchronous revokeObjectURL, asserting they transpile and round-trip. Both js and ts cases pass, as do the existing compile::worker_blob_url* specs.

Re: the compile spec command in the description — that refers to the pre-existing tests/specs/compile/worker_blob_url_revoke (added in #34574), which is why it wasn't in this PR's diff. I verified it (and compile::worker_blob_url) still pass with the standalone-loader change.

@divybot

divybot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor Author

Heads up on the one red check (test specs (2/2) debug linux-aarch64): the failure is specs::serve::request_signal_streaming, which is unrelated to this PR — it's a deterministic breakage on main since #34397 added the legacy-abort deprecation warning without updating that test's main.out. It fails on every PR that merges main, and the fix is the separate PR #35191 (still open). This PR's own tests (worker::worker_blob_url_revoke::{js,ts}, compile::worker_blob_url*) all pass on that same aarch64 run; the other 71 checks are green. Nothing to change here — it'll clear once #35191 lands and I merge main.

divybot added 3 commits June 13, 2026 15:21
…ming

Merging main brings in #34397, which emits a legacy-abort deprecation
warning when a Deno.serve handler uses legacy request.signal abort on a
successful response. The request_signal_streaming spec exercises exactly
that path, so its expected output must include the warning line. Same fix
as the closed #35191 (folded into #35133); included here so this PR's CI
is self-sufficient. Unrelated to the blob-worker change.
The 'test specs (2/2) release linux-x86_64' job failed in the
'Set up incremental LTO and sysroot build' step because apt could not
reach apt.llvm.org (Network is unreachable / connection timed out) while
installing LLVM packages — before any test ran. No source change; lacking
admin rerun rights, an empty commit re-triggers CI on a fresh runner.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Worker constructor should synchronously fetch URL content if it's a blob/object URL (i.e. created with URL.createObjectURL)

2 participants