Skip to content

Fix Notebook tab blocked by COEP (jupyter iframe refused to connect)#380

Merged
andeplane merged 3 commits into
mainfrom
fix/notebook-iframe-coep
Jul 11, 2026
Merged

Fix Notebook tab blocked by COEP (jupyter iframe refused to connect)#380
andeplane merged 3 commits into
mainfrom
fix/notebook-iframe-coep

Conversation

@andeplane

Copy link
Copy Markdown
Owner

Problem

Since the KOKKOS build made the page cross-origin isolated (COOP/COEP via coi-serviceworker on GitHub Pages), the Notebook tab shows "andeplane.github.io refused to connect" — the JupyterLite iframe is blocked with ERR_BLOCKED_BY_RESPONSE / blocked:COEP.

A cross-origin-isolated document may only embed iframes whose responses also carry a COEP header. The Notebook iframe (/atomify/jupyter/lab/index.html) is controlled by JupyterLite's own service worker, whose scope (jupyter/) is more specific than the coi shim's (/atomify/) — so the shim never sees those requests, and JupyterLite's worker serves the iframe document without COEP headers. That's also why it works exactly once: on the very first visit JupyterLite's service worker isn't registered yet, so the coi shim handles the iframe and injects the headers; every visit after that is blocked.

Fix

  • scripts/patch-jupyterlite-sw.mjs — post-processes the built jupyter/service-worker.js, wrapping its single GET response path (maybeFromCache) so every response it serves (cache hits and network fetches, including the iframe navigation) gets Cross-Origin-Embedder-Policy: credentialless + Cross-Origin-Opener-Policy: same-origin, matching the coi shim. Fails loudly if a future JupyterLite version renames the function.
  • deploy.yaml — runs the patch script after jupyter lite build.
  • index.html — nudges an already-registered (stale, broken) JupyterLite service worker to update on page load, so existing visitors recover on their first visit after deploy instead of the second.
  • .gitignore — ignore local jupyter lite build output.

Verification

Ran the production build in headless Chrome against a static server that sets no headers (GitHub Pages conditions):

Scenario Stock SW Patched SW
First visit (coi shim controls iframe) ✅ loads ✅ loads
Second visit (JupyterLite SW controls iframe) ERR_BLOCKED_BY_RESPONSE ✅ loads
Existing profile with stale broken SW, after deploy ✅ recovers on first visit

crossOriginIsolated remains true throughout, so the KOKKOS/SharedArrayBuffer path is unaffected. Typecheck passes.

🤖 Generated with Claude Code

Cross-origin isolation (COOP/COEP via coi-serviceworker, added for the
KOKKOS wasm build) broke the Notebook tab in production: a COEP page may
only embed iframes whose responses also carry a COEP header. The
JupyterLite iframe is controlled by JupyterLite's own service worker —
its scope (jupyter/) is more specific than the coi shim's, so its
responses had no COEP header and Chrome blocked the frame with
ERR_BLOCKED_BY_RESPONSE (blocked:COEP). It worked only on the very
first visit, before JupyterLite's service worker had registered.

Fix: post-process the built jupyter/service-worker.js to inject
COOP/COEP headers on every response it serves (wrapping its single GET
response path, maybeFromCache), wired into the deploy workflow. Also
nudge an already-registered stale JupyterLite service worker to update
on page load, so existing visitors recover on their first visit after
deploy instead of the second.

Verified in headless Chrome against a static server with no headers
(GitHub Pages conditions): stock service worker reproduces the block on
the second load; patched build loads the notebook on every visit, and a
profile with the stale service worker recovers immediately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a cross-origin isolation issue where the JupyterLite Notebook iframe was being blocked by the browser due to missing COEP/COOP headers. Since the application is cross-origin isolated, it requires all embedded iframes to provide these headers. Because JupyterLite manages its own service worker, it was bypassing the global shim. The solution involves patching the JupyterLite service worker at build time to include the necessary headers and ensuring existing service worker registrations are updated to apply these changes.

Highlights

  • COEP/COOP Header Injection: Introduced a post-processing script to inject required cross-origin isolation headers into the JupyterLite service worker, resolving iframe connection errors.
  • Service Worker Lifecycle Management: Updated the main index page to force an update of the JupyterLite service worker on load, ensuring users receive the patched version immediately.
  • Build Configuration: Updated .gitignore to exclude local JupyterLite build artifacts.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/deploy.yaml
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a mechanism to patch the JupyterLite service worker to inject COOP/COEP headers, allowing the Notebook iframe to be embedded in a cross-origin-isolated page. It also updates .gitignore to exclude JupyterLite build outputs and adds a script in index.html to force-update the service worker registration on each visit. The feedback suggests handling null-body status codes and preserving metadata (such as url and redirected) when constructing new Response objects in the patch script, as well as catching potential promise rejections from reg.update() in index.html.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread scripts/patch-jupyterlite-sw.mjs Outdated
Comment on lines +43 to +47
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

When constructing a new Response object, there are two critical issues to handle:

  1. Null Body Status Codes: Under the Fetch specification, responses with status codes 101, 103, 204, 205, or 304 must not have a body. If response.body is a non-null ReadableStream (even if empty), passing it to new Response with one of these status codes will throw a TypeError and crash the service worker.
  2. Loss of Metadata: Constructing a new Response loses the original url and redirected properties (they default to "" and false respectively). This can break relative URL resolution for subresources served by the service worker.

We can fix both issues by ensuring the body is null for null-body status codes, and using Object.defineProperty to preserve the url and redirected properties on the newly constructed response.

  const isNullBodyStatus = [101, 103, 204, 205, 304].includes(response.status);
  const body = isNullBodyStatus ? null : response.body;

  const patchedResponse = new Response(body, {
    status: response.status,
    statusText: response.statusText,
    headers,
  });

  if (response.url) {
    Object.defineProperty(patchedResponse, "url", { value: response.url });
  }
  if (response.redirected) {
    Object.defineProperty(patchedResponse, "redirected", { value: response.redirected });
  }

  return patchedResponse;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Done in 0208e54 — null body for null-body statuses, url/redirected preserved, and the whole injection wrapped in try/catch so the fetch handler can never crash on an unexpected response shape (falls back to the un-patched response).

Comment thread index.html Outdated
reg.active &&
reg.active.scriptURL.includes("/jupyter/service-worker")
) {
reg.update();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

reg.update() returns a Promise that can reject (for example, if the network is offline or the server returns an error). To prevent unhandled promise rejections in the browser console (which can trigger global error handlers or crash reporting tools), it is recommended to catch and ignore the rejection.

Suggested change
reg.update();
reg.update().catch(() => {});

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Done in 0208e54 — update() rejections are now caught. The nudge also updates all registrations (coi shim included), and /atomify/?sw-reset was added as a deterministic escape hatch that unregisters every service worker and clears CacheStorage.

andeplane and others added 2 commits July 11, 2026 09:08
Review (gemini): construct patched responses with a null body for
null-body statuses (204/205/304 etc.), preserve url/redirected, and
catch reg.update() rejections. Also wrap the header injection in
try/catch so an unexpected response shape can never crash the
JupyterLite service worker's fetch handler.

Deterministic updates: the page now calls reg.update() on ALL service
worker registrations (coi shim + JupyterLite) on every visit — both
workers use skipWaiting + clients.claim, so a deployed fix takes over
on the next load instead of waiting for browser update heuristics. As a
last-resort escape hatch, /atomify/?sw-reset unregisters every service
worker on the origin, clears CacheStorage, and reloads clean
(documented in README).

Verified in headless Chrome under GitHub Pages conditions: stale-broken-
SW profile recovers on first visit after deploy; ?sw-reset unsticks a
wedged profile even with the broken worker still deployed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ?sw-reset only unregisters service workers scoped under /atomify/ and
  deletes JupyterLite's "precache" cache, instead of wiping the whole
  andeplane.github.io origin (GitHub Pages projects share it).
- src/index.tsx skips booting the app during a ?sw-reset pass, so the
  reset no longer races app startup (wasm downloads, non-isolated boot)
  before the clean reload.
- The service worker patch now wraps FetchEvent.prototype.respondWith
  instead of monkey-patching the internal minified maybeFromCache
  binding: every response exit point gets the COEP headers and the patch
  no longer depends on an upstream internal name. Reconstruction
  failures are logged via console.warn instead of swallowed silently.
- Re-running the patch script replaces a previously applied patch
  (truncate at marker) instead of skipping, so template edits always
  take effect.
- The update-nudge and reset logic live in one script block; the nudge
  is skipped while resetting and getRegistrations() rejections are
  caught.

Re-verified in headless Chrome under GitHub Pages conditions: stale
broken-SW profile recovers on first visit after deploy; ?sw-reset
unsticks a wedged profile and lands on a clean URL with
crossOriginIsolated intact. Typecheck and vitest (430 tests) pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@andeplane
andeplane enabled auto-merge July 11, 2026 07:25
@andeplane
andeplane disabled auto-merge July 11, 2026 07:25
@andeplane
andeplane merged commit 8fb6203 into main Jul 11, 2026
2 checks passed
@andeplane
andeplane deleted the fix/notebook-iframe-coep branch July 11, 2026 07:32
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