Skip to content

fix: disable Jupyter binary WebSocket protocol for Bun compatibility#324

Merged
dinohamzic merged 4 commits intomainfrom
fix-jupyter-websocket-bun-compatibility
Mar 2, 2026
Merged

fix: disable Jupyter binary WebSocket protocol for Bun compatibility#324
dinohamzic merged 4 commits intomainfrom
fix-jupyter-websocket-bun-compatibility

Conversation

@dinohamzic
Copy link
Contributor

@dinohamzic dinohamzic commented Feb 26, 2026

The PyPI build (bun build --compile) crashes with RangeError: Out of bounds access when @jupyterlab/services deserializes binary WebSocket messages using the v1.kernel.websocket.jupyter.org protocol. This fix excludes the binary protocol from WebSocket negotiation via the documented ServerConnection.makeSettings({ WebSocket }) API, forcing JSON-based messaging which works correctly on both Node.js and Bun runtimes.

The trade-off is marginally higher serialization overhead for kernel messages, which is negligible for a CLI tool where Python execution time dominates.

Summary by CodeRabbit

  • Bug Fixes

    • Improved WebSocket negotiation to enforce JSON-only messaging for more reliable and consistent connections.
  • Tests

    • Expanded tests to validate WebSocket configuration, confirm a WebSocket factory is supplied, and ensure JSON-only negotiation behavior is preserved.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 26, 2026

📝 Walkthrough

Walkthrough

Adds a JSON-only WebSocket negotiation filter: introduces a JUPYTER_BINARY_PROTOCOL constant and a createJsonWebSocketFactory() that removes the Jupyter binary protocol from the Sec-WebSocket-Protocol list. The factory is injected into ServerConnection.makeSettings() via the WebSocket option so created sockets negotiate JSON-only protocols. Tests were updated to accept partial server settings and a new test asserts a WebSocket factory is passed and is a function.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant ServerConnection
    participant WebSocketFactory
    participant WebSocket
    participant JupyterKernel

    Client->>ServerConnection: makeSettings(baseUrl, wsUrl, WebSocket?)
    ServerConnection->>WebSocketFactory: call factory(url, protocols)
    Note right of WebSocketFactory: createJsonWebSocketFactory strips JUPYTER_BINARY_PROTOCOL
    WebSocketFactory->>WebSocket: new WebSocket(url, filteredProtocols)
    Client->>WebSocket: open connection
    WebSocket->>JupyterKernel: negotiate protocols (JSON-only)
    JupyterKernel->>WebSocket: handshake complete
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title accurately describes the main change: disabling Jupyter's binary WebSocket protocol for Bun compatibility via a targeted WebSocket factory wrapper.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Updates Docs ✅ Passed PR is a bug fix for internal Bun runtime compatibility. No public APIs modified; documentation updates not required.

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


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

@dinohamzic dinohamzic self-assigned this Feb 26, 2026
@codecov
Copy link

codecov bot commented Feb 26, 2026

Codecov Report

❌ Patch coverage is 30.00000% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.42%. Comparing base (4f5b20f) to head (f3a7a85).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
packages/runtime-core/src/kernel-client.ts 30.00% 7 Missing ⚠️

❌ Your patch status has failed because the patch coverage (30.00%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #324      +/-   ##
==========================================
- Coverage   83.49%   83.42%   -0.08%     
==========================================
  Files         122      122              
  Lines        7345     7355      +10     
  Branches     2035     2040       +5     
==========================================
+ Hits         6133     6136       +3     
- Misses       1212     1219       +7     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/runtime-core/src/kernel-client.test.ts`:
- Around line 119-125: The test only checks that a WebSocket function was passed
to mockMakeSettings but not that the binary protocol was removed; update the
test inside the it('passes JsonOnlyWebSocket to server settings', ...) block to
assert the filtering behavior by obtaining callArg =
mockMakeSettings.mock.calls[0][0], then either compare callArg.WebSocket to the
exported JsonOnlyWebSocket wrapper (JsonOnlyWebSocket) or instantiate the
provided constructor (new callArg.WebSocket(...)) and verify it does not accept
binary frames (e.g., sending an ArrayBuffer throws or is serialized to a string
/ binaryType is not set), so the test asserts the binary protocol is actually
removed rather than only checking for a function.
- Line 122: The test reads a potentially stale invocation via
mockMakeSettings.mock.calls[0][0]; update the test to either reset
mockMakeSettings in a beforeEach (call mockMakeSettings.mockClear() or
mockMakeSettings.mockReset()) or read the most recent invocation (use
mockMakeSettings.mock.lastCall or mockMakeSettings.mock.calls.at(-1)[0]) when
assigning callArg so the assertion always inspects the latest call to
mockMakeSettings.

In `@packages/runtime-core/src/kernel-client.ts`:
- Around line 30-34: The constructor in createJsonWebSocketFactory returns
single-string protocols unchanged, so if protocols === JUPYTER_BINARY_PROTOCOL
the binary protocol still gets negotiated; update the constructor in the class
returned by createJsonWebSocketFactory to normalize protocols whether it's an
array or a string (e.g., if protocols is a string and equals
JUPYTER_BINARY_PROTOCOL treat it as undefined/removed, otherwise keep it), then
pass the filtered/normalized value to super(url, filtered) — reference the
createJsonWebSocketFactory function, the inner class constructor, the protocols
parameter, and JUPYTER_BINARY_PROTOCOL to locate and fix the logic.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f416207 and d79bf33.

📒 Files selected for processing (2)
  • packages/runtime-core/src/kernel-client.test.ts
  • packages/runtime-core/src/kernel-client.ts

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)
packages/runtime-core/src/kernel-client.test.ts (1)

119-125: ⚠️ Potential issue | 🟡 Minor

Test verifies presence, not filtering behavior.

This confirms a WebSocket factory is passed but doesn't verify that v1.kernel.websocket.jupyter.org gets filtered out. The core fix remains unguarded.

Consider instantiating the factory and asserting the protocol is excluded:

it('WebSocket factory filters out binary protocol', async () => {
  await client.connect('http://localhost:8888')
  
  const callArg = mockMakeSettings.mock.calls[mockMakeSettings.mock.calls.length - 1][0]
  const WebSocketFactory = callArg.WebSocket as typeof WebSocket
  
  // Verify binary protocol is filtered from array
  const ws = new WebSocketFactory('ws://test', ['v1.kernel.websocket.jupyter.org', 'other'])
  // Would need to capture what super() received - alternatively test createJsonWebSocketFactory directly
})

Or export and unit test createJsonWebSocketFactory directly for cleaner assertions.

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

In `@packages/runtime-core/src/kernel-client.test.ts` around lines 119 - 125, The
test currently only checks that a WebSocket factory is passed (mockMakeSettings
call) but does not assert that the unwanted protocol
'v1.kernel.websocket.jupyter.org' is filtered; update the test to either (a)
instantiate the passed WebSocket factory from the last mockMakeSettings call
(inspect callArg.WebSocket) and wrap/spy the underlying WebSocket constructor to
capture the protocols array so you can assert the binary protocol is omitted, or
(b) export and unit-test the createJsonWebSocketFactory function directly and
assert that when given protocols including 'v1.kernel.websocket.jupyter.org' it
returns/constructs with that protocol removed; reference mockMakeSettings,
callArg.WebSocket and createJsonWebSocketFactory in the fix.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/runtime-core/src/kernel-client.test.ts`:
- Around line 32-35: The mock function mockMakeSettings currently accepts a
WebSocket parameter but omits it from the returned settings object; update the
mockMakeSettings implementation so its return value includes the passed
WebSocket (e.g., return { baseUrl: config.baseUrl, wsUrl: config.wsUrl,
WebSocket: config.WebSocket }) to mirror the real factory shape and ensure tests
preserve the WebSocket factory behavior.

---

Duplicate comments:
In `@packages/runtime-core/src/kernel-client.test.ts`:
- Around line 119-125: The test currently only checks that a WebSocket factory
is passed (mockMakeSettings call) but does not assert that the unwanted protocol
'v1.kernel.websocket.jupyter.org' is filtered; update the test to either (a)
instantiate the passed WebSocket factory from the last mockMakeSettings call
(inspect callArg.WebSocket) and wrap/spy the underlying WebSocket constructor to
capture the protocols array so you can assert the binary protocol is omitted, or
(b) export and unit-test the createJsonWebSocketFactory function directly and
assert that when given protocols including 'v1.kernel.websocket.jupyter.org' it
returns/constructs with that protocol removed; reference mockMakeSettings,
callArg.WebSocket and createJsonWebSocketFactory in the fix.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d79bf33 and 432ba0e.

📒 Files selected for processing (2)
  • packages/runtime-core/src/kernel-client.test.ts
  • packages/runtime-core/src/kernel-client.ts

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.

♻️ Duplicate comments (1)
packages/runtime-core/src/kernel-client.test.ts (1)

120-125: ⚠️ Potential issue | 🟠 Major

Test the protocol filtering, not just function presence.

Line 123-Line 125 confirms only that WebSocket is a function. It still does not prove v1.kernel.websocket.jupyter.org is removed, so the Bun fix can regress silently.

♻️ Proposed test update
-    it('passes a WebSocket factory to server settings', async () => {
+    it('filters out Jupyter binary protocol from negotiated protocols', async () => {
+      const wsCtor = vi.fn()
+      vi.stubGlobal('WebSocket', wsCtor)
+
       await client.connect('http://localhost:8888')
 
-      const callArg = mockMakeSettings.mock.calls[mockMakeSettings.mock.calls.length - 1][0]
-      expect(callArg).toHaveProperty('WebSocket')
-      expect(typeof callArg.WebSocket).toBe('function')
+      const callArg = mockMakeSettings.mock.lastCall?.[0]
+      expect(callArg?.WebSocket).toEqual(expect.any(Function))
+
+      const protocols = ['v1.kernel.websocket.jupyter.org', 'json']
+      new (callArg!.WebSocket as typeof globalThis.WebSocket)('ws://localhost:8888/', protocols)
+
+      expect(wsCtor).toHaveBeenCalled()
+      const [, negotiatedProtocols] = wsCtor.mock.calls[0]
+      expect(negotiatedProtocols).toEqual(
+        expect.not.arrayContaining(['v1.kernel.websocket.jupyter.org'])
+      )
+      expect(negotiatedProtocols).toEqual(expect.arrayContaining(['json']))
+
+      vi.unstubAllGlobals()
     })

As per coding guidelines, "Create comprehensive tests for all new features using Vitest" and "Test edge cases, error handling, and special characters in test files".

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

In `@packages/runtime-core/src/kernel-client.test.ts` around lines 120 - 125, The
test only checks that settings.WebSocket is a function but not that it filters
out the banned protocol; update the test to call the factory function returned
in mockMakeSettings (grabbed via mockMakeSettings.mock.calls[...] ->
callArg.WebSocket) with a protocols array that includes
'v1.kernel.websocket.jupyter.org' and verify the underlying WebSocket
constructor/spy receives a protocols array that does NOT contain
'v1.kernel.websocket.jupyter.org' (and still includes other protocols), ensuring
protocol filtering behavior is asserted rather than just function presence.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/runtime-core/src/kernel-client.test.ts`:
- Around line 120-125: The test only checks that settings.WebSocket is a
function but not that it filters out the banned protocol; update the test to
call the factory function returned in mockMakeSettings (grabbed via
mockMakeSettings.mock.calls[...] -> callArg.WebSocket) with a protocols array
that includes 'v1.kernel.websocket.jupyter.org' and verify the underlying
WebSocket constructor/spy receives a protocols array that does NOT contain
'v1.kernel.websocket.jupyter.org' (and still includes other protocols), ensuring
protocol filtering behavior is asserted rather than just function presence.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 432ba0e and 6d1d79c.

📒 Files selected for processing (1)
  • packages/runtime-core/src/kernel-client.test.ts

@dinohamzic dinohamzic marked this pull request as ready for review February 26, 2026 12:06
@dinohamzic dinohamzic requested a review from a team as a code owner February 26, 2026 12:06
@dinohamzic dinohamzic requested a review from tkislan February 26, 2026 12:06
@dinohamzic dinohamzic merged commit 9ffac58 into main Mar 2, 2026
18 of 21 checks passed
@dinohamzic dinohamzic deleted the fix-jupyter-websocket-bun-compatibility branch March 2, 2026 07:37
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.

2 participants