Skip to content

fix(js): always fall back to non-streaming WebAssembly.instantiate#445

Merged
sd2k merged 1 commit intomainfrom
fix/wasm-streaming-fallback
Feb 24, 2026
Merged

fix(js): always fall back to non-streaming WebAssembly.instantiate#445
sd2k merged 1 commit intomainfrom
fix/wasm-streaming-fallback

Conversation

@sd2k
Copy link
Collaborator

@sd2k sd2k commented Feb 23, 2026

Summary

  • Add a post-build step to js/justfile that patches wasm-bindgen's generated __wbg_load function to always fall back to the ArrayBuffer + WebAssembly.instantiate path when instantiateStreaming fails, instead of conditionally rethrowing

Problem

wasm-bindgen generates code that rethrows when WebAssembly.instantiateStreaming fails and Content-Type is application/wasm. This breaks in environments where fetch() returns a Proxy-wrapped Response (e.g. @opentelemetry/instrumentation-fetch) that passes JS instanceof Response but fails the browser's internal C++ slot checks.

This has been observed in Grafana instances with OpenTelemetry tracing enabled, causing @bsull/augurs WASM modules to fail to load with:

TypeError: WebAssembly: Expected Response or Promise resolving to Response

The OTel fetch instrumentation wraps globalThis.fetch and returns a Proxy<Response> from createResponseProxy(). This Proxy correctly forwards all JS-visible properties, but browsers (Firefox and Chrome) perform internal slot validation in instantiateStreaming/compileStreaming that rejects Proxy objects.

Fix

The streaming path is purely an optimization — the ArrayBuffer path (response.arrayBuffer() + WebAssembly.instantiate()) always works correctly regardless of whether the Response is a Proxy. The post-build Python script patches all generated JS files to replace the conditional rethrow with an unconditional console.warn + fallback.

Test plan

  • Run just build in js/ and verify all 10 .js files no longer contain throw e; or EXPECTED_RESPONSE_TYPES
  • Verify WASM modules load correctly in an environment with @opentelemetry/instrumentation-fetch active
  • Verify WASM modules still load correctly in a standard environment (no OTel)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a build task that applies an automated patch to ensure a consistent WebAssembly streaming fallback during builds.
  • Bug Fixes

    • Improved WebAssembly loading stability: on streaming errors the app now logs a warning and reliably falls back to standard instantiation instead of throwing, reducing runtime failures and improving cross-browser resilience.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 23, 2026

Walkthrough

Adds a Justfile recipe and patch script that modify wasm-bindgen–generated JS to always log a warning and fall back to WebAssembly.instantiate when instantiateStreaming fails, removes an unused EXPECTED_RESPONSE_TYPES constant, and verifies per-file patch results.

Changes

Cohort / File(s) Summary
Justfile
js/justfile
Adds fix-wasm-streaming-fallback recipe and inserts it into the build sequence.
Patch script
js/.../fix-wasm-streaming-fallback
New script that patches augurs/*.js: replaces conditional rethrow in __wbg_load for instantiateStreaming with an unconditional console.warn and fallback to WebAssembly.instantiate, removes EXPECTED_RESPONSE_TYPES, and handles different wasm-bindgen versions via dual regexes; reports per-file results and exits nonzero on failures.
Patched targets
js/augurs/*.js
Files expected to be modified by the patch script to change streaming failure handling and remove the unused constant.

Sequence Diagram(s)

sequenceDiagram
    rect rgba(220,240,255,0.5)
    participant Browser
    end
    rect rgba(240,255,220,0.5)
    participant WASMLoader as WASM Loader
    participant Network
    end
    rect rgba(255,240,220,0.5)
    participant WebAssemblyRuntime as WebAssembly
    end

    Browser->>WASMLoader: request module
    WASMLoader->>Network: fetch(wasmURL)
    Network-->>WASMLoader: Response (stream)
    WASMLoader->>WebAssemblyRuntime: try WebAssembly.instantiateStreaming(response, imports)
    alt instantiateStreaming succeeds
        WebAssemblyRuntime-->>WASMLoader: Module/Instance
        WASMLoader-->>Browser: initialize
    else instantiateStreaming throws
        WebAssemblyRuntime--xWASMLoader: throws
        WASMLoader->>Console: console.warn("instantiateStreaming failed, falling back")
        WASMLoader->>Network: response.arrayBuffer()
        Network-->>WASMLoader: ArrayBuffer
        WASMLoader->>WebAssemblyRuntime: WebAssembly.instantiate(buffer, imports)
        WebAssemblyRuntime-->>WASMLoader: Module/Instance
        WASMLoader-->>Browser: initialize
    end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Poem

🐰 I nudged the stream with careful paw and found a safer way,

A warning hopped out, then sync saved the day.
EXPECTED folded, tucked away from light,
Builds hum on steady through the quiet night. 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main change: adding a fallback mechanism to non-streaming WebAssembly.instantiate, which is the core purpose of the build patch. The title is concise, specific, and directly reflects the changeset content.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/wasm-streaming-fallback

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
js/justfile (1)

55-55: Use ([ \t]*) instead of (\s*) to capture only indentation, not preceding newlines

The pattern (\s*) at the start matches a preceding newline (from the previous line) plus the indentation spaces. While syntactically correct, using ([ \t]*) (horizontal whitespace only) makes the intent explicit and avoids the ambiguity of capturing a newline that gets re-emitted twice via \1 in the replacement string. This is particularly important for regex maintainability given the pattern's sensitivity to wasm-bindgen's exact code format.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@js/justfile` at line 55, Update the regex r'(\s*)\} catch \(e\) \{\s*\n' so
its leading capture only matches horizontal indentation, not newlines—replace
the group (\s*) with ([ \t]*) in the regex literal in js/justfile (the pattern
string used where the catch-block formatting is matched) so the backreference
(\1) will contain only spaces/tabs and won't re-emit a preceding newline.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@js/justfile`:
- Around line 43-69: The patcher currently uses re.sub which silently does
nothing if the regex doesn't match; update the fix-wasm-streaming-fallback
Python target to use re.subn for the big try/catch replacement and for the
EXPECTED_RESPONSE_TYPES removal ( operate on the same text variable ), check the
returned count for each substitution, and if any expected substitution count is
0, print a clear error mentioning the filename f and sys.exit(1) to fail the
build; also adjust the indentation capture group in the big regex from (\s*) to
([ \t]*) as suggested so it only matches spaces/tabs, keeping the rest of the
replacement logic and writing back the file when substitutions succeed.

---

Nitpick comments:
In `@js/justfile`:
- Line 55: Update the regex r'(\s*)\} catch \(e\) \{\s*\n' so its leading
capture only matches horizontal indentation, not newlines—replace the group
(\s*) with ([ \t]*) in the regex literal in js/justfile (the pattern string used
where the catch-block formatting is matched) so the backreference (\1) will
contain only spaces/tabs and won't re-emit a preceding newline.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0cfe8ce and 43f09f7.

📒 Files selected for processing (1)
  • js/justfile

@sd2k sd2k force-pushed the fix/wasm-streaming-fallback branch 2 times, most recently from 11a4b16 to 90d4ec1 Compare February 23, 2026 19:06
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
js/justfile (1)

82-82: ⚠️ Potential issue | 🟡 Minor

Silent success when no .js files are found in augurs/.

If glob.glob("augurs/*.js") returns an empty list (e.g., wasm-pack output directory changed), the for loop body never executes, errors stays empty, and the script exits 0 — silently skipping all patches with no signal.

🐛 Proposed fix — assert the glob is non-empty
+  files = sorted(glob.glob("augurs/*.js"))
+  assert files, "fix-wasm-streaming-fallback: no .js files found in augurs/"
-  for f in sorted(glob.glob("augurs/*.js")):
+  for f in files:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@js/justfile` at line 82, The loop over glob.glob("augurs/*.js") can silently
do nothing if no files match; before iterating, check the result of
glob.glob("augurs/*.js") (e.g., assign to a variable like augur_files) and if
it's empty, raise an exception or exit non-zero with a clear error message so
the script fails fast instead of leaving errors empty; modify the code that
currently iterates directly over glob.glob(...) and add this pre-check
referencing augur_files and errors to ensure a non-empty set of files is
required.
🧹 Nitpick comments (1)
js/justfile (1)

83-83: File handles opened without with context managers.

Both open(f).read() and open(f, "w").write(text) leave file descriptors unclosed until GC. In a short-lived script this is benign, but using with is the idiomatic and safe form, especially before the w write (ensures flush + close on exception).

♻️ Proposed refactor
-      text = open(f).read()
+      with open(f, encoding="utf-8") as fh:
+          text = fh.read()
-      open(f, "w").write(text)
+      with open(f, "w", encoding="utf-8") as fh:
+          fh.write(text)

Also applies to: 101-101

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@js/justfile` at line 83, Replace plain open(...) calls with context managers:
change the read site that uses open(f).read() to use a with open(f, "r") as fh:
text = fh.read() pattern, and change the write site that uses open(f,
"w").write(text) to with open(f, "w") as fh: fh.write(text); update occurrences
of open(f).read() and open(f, "w").write(text) accordingly so file descriptors
are guaranteed closed even on exceptions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@js/justfile`:
- Around line 97-100: The current whole-file check if "throw e;" in text risks
false positives; update the logic that verifies the patch by restricting the
search to the __wbg_load function body (or the exact replaced region) instead of
scanning the entire text. Locate the patched region using the same markers or
pattern used when replacing __wbg_load (e.g., the function signature
"__wbg_load" and its closing brace) and only check that substring for "throw
e;"; if present within that region append the error to errors, otherwise allow
the file to be written. Ensure you reference the existing variables (text,
errors) and the __wbg_load replacement code path so behavior remains consistent.

---

Duplicate comments:
In `@js/justfile`:
- Line 82: The loop over glob.glob("augurs/*.js") can silently do nothing if no
files match; before iterating, check the result of glob.glob("augurs/*.js")
(e.g., assign to a variable like augur_files) and if it's empty, raise an
exception or exit non-zero with a clear error message so the script fails fast
instead of leaving errors empty; modify the code that currently iterates
directly over glob.glob(...) and add this pre-check referencing augur_files and
errors to ensure a non-empty set of files is required.

---

Nitpick comments:
In `@js/justfile`:
- Line 83: Replace plain open(...) calls with context managers: change the read
site that uses open(f).read() to use a with open(f, "r") as fh: text = fh.read()
pattern, and change the write site that uses open(f, "w").write(text) to with
open(f, "w") as fh: fh.write(text); update occurrences of open(f).read() and
open(f, "w").write(text) accordingly so file descriptors are guaranteed closed
even on exceptions.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 11a4b16 and 90d4ec1.

📒 Files selected for processing (1)
  • js/justfile

js/justfile Outdated
Comment on lines 97 to 100
# Verify no throw e; remains in the __wbg_load function.
if "throw e;" in text:
errors.append(f"{f}: patched but 'throw e;' still present")
continue
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

"throw e;" in text is a whole-file scan — risk of false positives.

After patching, if "throw e;" in text searches the entire file content. If any other function in the wasm-bindgen–generated JS (e.g., error-propagation helpers added in future wasm-bindgen versions) contains the literal throw e; outside of __wbg_load, the check fires on a correctly-patched file: the error is appended, the file is not written, and the build exits 1 even though the patch was applied correctly.

Scope the check to just the __wbg_load function body, or at minimum confirm the substring is absent only within the replaced region:

🐛 Proposed fix — verify only the patched region
-      # Verify no throw e; remains in the __wbg_load function.
-      if "throw e;" in text:
-          errors.append(f"{f}: patched but 'throw e;' still present")
-          continue
+      # Verify the specific catch block no longer contains throw e;
+      # by re-searching for the original pattern (should be absent after patching).
+      if CATCH_V1.search(text) or CATCH_V2.search(text):
+          errors.append(f"{f}: patched but original rethrow pattern still present")
+          continue
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@js/justfile` around lines 97 - 100, The current whole-file check if "throw
e;" in text risks false positives; update the logic that verifies the patch by
restricting the search to the __wbg_load function body (or the exact replaced
region) instead of scanning the entire text. Locate the patched region using the
same markers or pattern used when replacing __wbg_load (e.g., the function
signature "__wbg_load" and its closing brace) and only check that substring for
"throw e;"; if present within that region append the error to errors, otherwise
allow the file to be written. Ensure you reference the existing variables (text,
errors) and the __wbg_load replacement code path so behavior remains consistent.

wasm-bindgen generates code that rethrows when
WebAssembly.instantiateStreaming fails and Content-Type is
application/wasm. This breaks in environments where fetch() returns a
Proxy-wrapped Response (e.g. OpenTelemetry instrumentation-fetch) that
passes JS `instanceof Response` but fails the browser's internal slot
checks in WebAssembly.instantiateStreaming/compileStreaming.

Add a post-build step that patches the generated __wbg_load function to
always fall back to the ArrayBuffer path (response.arrayBuffer() +
WebAssembly.instantiate) instead of conditionally rethrowing. The
streaming path is purely an optimization; the ArrayBuffer path is always
correct.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sd2k sd2k force-pushed the fix/wasm-streaming-fallback branch from 90d4ec1 to 7c68f15 Compare February 24, 2026 08:03
@sd2k sd2k merged commit 1ce32ec into main Feb 24, 2026
31 checks passed
@sd2k sd2k deleted the fix/wasm-streaming-fallback branch February 24, 2026 08:10
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
js/justfile (1)

82-83: Add a guard against an empty glob result.

If augurs/*.js is empty (e.g. all build-inner targets were somehow skipped or ran in a separate step), the loop body never executes, errors remains empty, and the script exits 0 — silently succeeding without patching a single file. This also combines with the missing context-manager pattern on line 83 and line 101.

🛡️ Proposed fix — empty-glob guard + context managers
-  errors = []
-  for f in sorted(glob.glob("augurs/*.js")):
-      text = open(f).read()
+  files = sorted(glob.glob("augurs/*.js"))
+  if not files:
+      print("fix-wasm-streaming-fallback: no .js files found in augurs/ — was the build run first?", file=sys.stderr)
+      sys.exit(1)
+  errors = []
+  for f in files:
+      with open(f) as fh:
+          text = fh.read()
-      open(f, "w").write(text)
+      with open(f, "w") as fh:
+          fh.write(text)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@js/justfile` around lines 82 - 83, Add an explicit guard for an empty glob
result before iterating over sorted(glob.glob("augurs/*.js")): if the list is
empty, log or raise an error and exit non‑zero so the script doesn't silently
succeed; also replace open(f).read() with the context-manager pattern (with
open(f, "r") as fh: text = fh.read()) and apply the same change where files are
opened later (e.g., the other open(...) at line 101) so files are always closed;
reference the glob call sorted(glob.glob("augurs/*.js")), the loop variable f,
the variable text, and the errors accumulator when implementing the guard and
exit behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@js/justfile`:
- Around line 88-93: The current logic appends an error when both CATCH_V2.subn
and CATCH_V1.subn return n == 0 even if the file is already patched; modify the
branch after those two attempts to detect an already-patched file (e.g., check
if CATCH_REPL or a unique marker is present in text) and if so skip adding the
error and continue, otherwise keep appending the existing error; adjust the code
around the variables CATCH_V2, CATCH_V1, CATCH_REPL, text, errors and f so the
recipe is idempotent when run against already-patched files.

---

Nitpick comments:
In `@js/justfile`:
- Around line 82-83: Add an explicit guard for an empty glob result before
iterating over sorted(glob.glob("augurs/*.js")): if the list is empty, log or
raise an error and exit non‑zero so the script doesn't silently succeed; also
replace open(f).read() with the context-manager pattern (with open(f, "r") as
fh: text = fh.read()) and apply the same change where files are opened later
(e.g., the other open(...) at line 101) so files are always closed; reference
the glob call sorted(glob.glob("augurs/*.js")), the loop variable f, the
variable text, and the errors accumulator when implementing the guard and exit
behavior.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 90d4ec1 and 7c68f15.

📒 Files selected for processing (1)
  • js/justfile

Comment on lines +88 to +93
text, n = CATCH_V2.subn(CATCH_REPL, text)
if n == 0:
text, n = CATCH_V1.subn(CATCH_REPL, text)
if n == 0:
errors.append(f"{f}: expected to patch __wbg_load but regex didn't match")
continue
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Idempotency gap: running the recipe standalone on already-patched files produces a misleading error.

When both CATCH_V2.subn and CATCH_V1.subn return n == 0 on a previously-patched file, the error appended is "expected to patch __wbg_load but regex didn't match", and the final message printed is "The wasm-bindgen output format may have changed. Update the regex in js/justfile." A developer retrying without a clean rebuild (e.g., after a partial build failure) would chase a phantom wasm-bindgen format change. A simple content-based already-patched check avoids the false alarm:

♻️ Proposed fix — distinguish already-patched from genuinely unrecognised format
      if n == 0:
-         errors.append(f"{f}: expected to patch __wbg_load but regex didn't match")
-         continue
+         # If neither pattern is present either, the file may already be patched.
+         if not (CATCH_V1.search(text) or CATCH_V2.search(text)):
+             print(f"  {f}: already patched or no matching pattern — skipping")
+             continue
+         errors.append(f"{f}: instantiateStreaming present but regex didn't match; "
+                        f"wasm-bindgen output format may have changed")
+         continue
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@js/justfile` around lines 88 - 93, The current logic appends an error when
both CATCH_V2.subn and CATCH_V1.subn return n == 0 even if the file is already
patched; modify the branch after those two attempts to detect an already-patched
file (e.g., check if CATCH_REPL or a unique marker is present in text) and if so
skip adding the error and continue, otherwise keep appending the existing error;
adjust the code around the variables CATCH_V2, CATCH_V1, CATCH_REPL, text,
errors and f so the recipe is idempotent when run against already-patched files.

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.

1 participant