Skip to content

undici: export ping() and publish undici:websocket:ping/pong diagnostics channels - #37111

Open
robobun wants to merge 6 commits into
mainfrom
farm/7076b23d/undici-websocket-ping
Open

robobun wants to merge 6 commits into
mainfrom
farm/7076b23d/undici-websocket-ping

Conversation

@robobun

@robobun robobun commented Aug 7, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #37110.

What does this PR do?

Bun aliases the bare undici specifier to its builtin shim, and the shim covered neither the module-level ping() export nor the undici:websocket:ping/undici:websocket:pong diagnostics channels that undici v7 publishes. @slack/socket-mode@3.x (the transport under Bolt's SocketModeReceiver) uses both, so every connection died on its first heartbeat with TypeError: ping is not a function and the client reconnected forever; even with that swallowed, its health monitor would count 3 missed pongs (the pong channel never fires) and disconnect anyway.

// with undici@7.29.0 installed in node_modules
import * as undici from "undici";
console.log(typeof undici.ping);
// before: "undefined"   after (and on node): "function"

How

Shim-level changes in src/js/thirdparty/undici.js, no native changes:

  • ping(ws, payload) mirrors undici's semantics: payload must be a Buffer or undefined and at most 125 bytes (TypeError otherwise, with undici's messages), and the frame is only sent while the socket is open (no-op otherwise). It delegates to the native WebSocket's nonstandard ping() method, the same one the ws shim already relies on.
  • The shim's exported WebSocket is now a subclass of the native class that listens for the native ping/pong events and publishes { payload, websocket } (payload is a Buffer) to the undici:websocket:ping/undici:websocket:pong channels when they have subscribers, matching undici's message shape. @slack/socket-mode constructs its socket via new (require("undici").WebSocket)(...) and identity-checks message.websocket, so the subclass covers it.

Related: #36102 would resolve bare undici to the installed package when present (which also fixes this scenario when the real package is installed), and #34635 publishes the websocket channels natively for all WebSocket instances. This PR keeps the shim itself correct for this surface and stands alone; if #34635 lands, its native publish supersedes the subclass listeners here.

Verification

New tests in test/js/first_party/undici/undici.test.ts:

  • ping is exported as a function (fails on current bun with SyntaxError: Export named 'ping' not found in module 'undici')
  • ping(ws, payload) delivers a ping frame to a Bun.serve websocket server, and the server's pong reply publishes on undici:websocket:pong with the echoed Buffer payload and the originating websocket
  • a server-initiated ping publishes on undici:websocket:ping
  • argument validation: non-WebSocket receiver and non-Buffer payload throw TypeError, payloads over 125 bytes throw, and pinging a not-yet-open socket is a no-op

bun bd test test/js/first_party/undici/undici.test.ts: 14 pass, 0 fail. The pre-existing undici-primordials.test.ts also passes.


[review] gate passed · iteration 2 · 2 files touched

fails on main (without fix)
ASAN without fix: BUILD FAILED (no junit output)
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/first_party/undici/undici.test.ts
bun test v1.4.0 (6e906e468)

test/js/first_party/undici/undici.test.ts:

# Unhandled error between tests
-------------------------------
SyntaxError: Export named 'ping' not found in module 'undici'.
-------------------------------


 0 pass
 1 fail
 1 error
Ran 1 test across 1 file. [2.77s]
error: script "bd" exited with code 1
__F:-1:S:0

release without fix: BUILD FAILED (no junit output)
bun test v1.4.0-canary.1 (6e906e468)

test/js/first_party/undici/undici.test.ts:

# Unhandled error between tests
-------------------------------
SyntaxError: Export named 'ping' not found in module 'undici'.
-------------------------------


 0 pass
 1 fail
 1 error
Ran 1 test across 1 file. [174.00ms]
__F:-1:S:0
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/first_party/undici/undici.test.ts
bun test v1.4.0 (6e906e468)

test/js/first_party/undici/undici.test.ts:
(pass) undici > request > should make a GET request when passed a URL string [74.98ms]
(pass) undici > request > should error when body has already been consumed [18.13ms]
(pass) undici > request > should make a POST request when provided a body and POST method [17.54ms]
(pass) undici > request > should stream a node:stream Readable body [252.30ms]
(pass) undici > request > should accept a URL class object [11.14ms]
(pass) undici > request > should prevent body from being attached to GET or HEAD requests [12.35ms]
(pass) undici > request > a 204 has no body [55.50ms]
(pass) undici > request > the response to a HEAD request has no body [22.66ms]
(pass) undici > request > should allow a query string to be passed [20.83ms]
(pass) undici > request > should throw on HTTP 4xx or 5xx error when throwOnError is true [19.09ms]
(pass) undici > request > should allow us to abort the request with a signal [533.38ms]
(pass) undici > req
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     139022da2b
  features     baseline

23 deps, 129 codegen, 1172 objects in 684ms

ninja: Entering directory `/workspace/bun/build/release'
[1/1244] gen ErrorCode+*.h
[2/1244] install /workspace/bun
bun install v1.4.0-canary.1 (6e906e468)

Checked 26 installs across 63 packages (no changes) [8.00ms]
[3/1244] install /workspace/bun/packages/bun-error
bun install v1.4.0-canary.1 (6e906e468)

Checked 1 install across 2 packages (no changes) [4.00ms]
[4/1244] fetch tinycc
[tinycc] up to date
[5/1243] gen bindgenv2
[6/1243] install /workspace/bun/src/node-fallbacks
bun install v1.4.0-canary.1 (6e906e468)

Checked 111 installs across 104 packages (no changes) [9.00ms]
[7/1243] fetch libjpeg-turbo
[libjpeg-turbo] up to date
[8/1216] fetch zlib
[zlib] up to date
[9/1216] gen .bind.ts → GeneratedBindings.cpp
[10/1216] gen node-fallbacks/react-refresh.js
Bundled 1 module in 7ms

  react-refresh.js  4.81 KB  (entry point)

[11/1216] gen ProcessBindingConstants.lut.h
Generating /workspace/bun/build
... (truncated)
diff hotspot
src/js/thirdparty/undici.js               |  51 ++++++++++-
 test/js/first_party/undici/undici.test.ts | 140 +++++++++++++++++++++++++++++-
 2 files changed, 189 insertions(+), 2 deletions(-)

gate history · 4 passed · 1 rejected · iteration 2

evidence per changed file
file                                       reads  edits  tests
src/js/thirdparty/undici.js                    7     13      0
test/js/first_party/undici/undici.test.ts      5     10      0

@coderabbitai

coderabbitai Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

WebSocket ping support

Layer / File(s) Summary
Ping implementation
src/js/thirdparty/undici.js
The Undici WebSocket wrapper publishes ping and pong diagnostics. The exported ping helper validates the socket, payload, payload size, and connection state before sending.
WebSocket ping tests
test/js/first_party/undici/undici.test.ts
Tests cover the export, frame payloads, diagnostics, socket identities, invalid arguments, oversized payloads, and connecting-state calls.

Possibly related issues

  • oven-sh/bun#37110: Describes the missing ping() export and WebSocket ping/pong diagnostics implemented by this change.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely identifies the exported ping API and WebSocket diagnostics channels added by the pull request.
Description check ✅ Passed The description includes the required sections, explains the implementation and motivation, and documents verification results with passing tests.

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

@coderabbitai coderabbitai 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.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/js/thirdparty/undici.js`:
- Line 3: The module eagerly requires diagnostics_channel and creates its
channels even when WebSockets are unused; move the require and channel
initialization into the WebSocket constructor and ping paths, reusing a lazy
shared initialization so both paths obtain the channels only when needed while
retaining the node:-prefixed require.
- Around line 413-416: Update the WebSocket ping logic around BunWebSocket.OPEN
to throw InvalidStateError when readyState is CONNECTING, while keeping CLOSING
and CLOSED as no-ops. Preserve the existing payload and no-payload ws.ping()
calls for the OPEN state.
- Around line 400-411: Update the receiver validation in ping to check the
Undici wrapper WebSocket brand rather than BunWebSocket, so native Bun WebSocket
instances are rejected consistently with the private handler brand check.
Preserve the existing Buffer|undefined payload validation and 125-byte limit.

In `@test/js/first_party/undici/undici.test.ts`:
- Line 212: Change the “undici WebSocket ping” suite to describe.concurrent so
its independent network tests run in parallel. Update each diagnostics-channel
subscriber used by the suite to filter messages by message.websocket before
resolving, preserving isolation from other concurrent clients.
- Around line 290-296: Add the missing argument-validation cases to the existing
“ping() validates its arguments like undici” test: cover null payloads,
Uint8Array payloads, empty-string and zero payloads, and undefined/null
websocket arguments. Assert the shim’s expected behavior for each variant,
matching the validation logic in ping and preserving the existing boundary and
invalid-type assertions.
- Around line 290-303: Update the ping() validation test to assert ws.readyState
is not OPEN immediately before the no-op assertions, ensuring the socket remains
in the not-open state when testing ping(ws, ...) and ping(ws,
Buffer.alloc(125)).
- Around line 266-288: Update the “undici:websocket:ping fires when the server
sends a ping” test so its promise resolver rejects on WebSocket error and close
events, matching the failure-handling pattern used by the first opened test.
Register these listeners before awaiting pingMessage.promise and retain the
existing cleanup and ping assertions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ddb70a85-b071-4e4c-b8d5-b86448930a54

📥 Commits

Reviewing files that changed from the base of the PR and between 45ee955 and 128cf8f.

📒 Files selected for processing (2)
  • src/js/thirdparty/undici.js
  • test/js/first_party/undici/undici.test.ts

Comment thread src/js/thirdparty/undici.js
Comment thread src/js/thirdparty/undici.js
Comment thread src/js/thirdparty/undici.js
Comment thread test/js/first_party/undici/undici.test.ts Outdated
Comment thread test/js/first_party/undici/undici.test.ts
Comment thread test/js/first_party/undici/undici.test.ts
Comment thread test/js/first_party/undici/undici.test.ts
Comment thread src/js/thirdparty/undici.js Outdated
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. fetch, WebSocket: publish undici:* diagnostics_channel events #34635 - Already implements the undici:websocket:ping/pong diagnostics channels (plus open/close/socket_error), natively in WebSocket.cpp rather than in the JS shim.
  2. Resolve bare undici to the installed package, keep the shim as fallback #36102 - Resolves bare undici to the installed npm package (shim as fallback), which is the primary expected behavior undici shim lacks ping() export and undici:websocket:ping/pong diagnostics channels, breaking @slack/socket-mode (permanent reconnect loop) #37110 asks for and would supply ping() and the channels from real undici.
  3. undici: remove polyfill, resolve to real npm package #30561 - Removes src/js/thirdparty/undici.js and its tests entirely so bare undici resolves to the real package, addressing the same failure class by deleting the file this PR extends.

🤖 Generated with Claude Code

@robobun

robobun commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

Not a duplicate, though all three are related and already linked in the description:

This PR is the minimal self-contained fix for #37110: as long as the shim exists and answers bare undici, it needs this surface. If the resolution PRs land, the shim (and this code) simply stops being used when the real package is present; if #34635 lands, its native channel publishing supersedes the subclass listeners here and the ping() export remains.

Comment thread src/js/thirdparty/undici.js
Comment thread test/js/first_party/undici/undici.test.ts Outdated

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/js/thirdparty/undici.js`:
- Around line 382-383: Capture stable references to the ArrayBuffer constructor
and its isView method during module initialization, then update the data
conversion checks in the buffer-handling path to use those captured intrinsics
instead of mutable globals. Preserve the existing byteOffset and byteLength
behavior for views.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c9fdf175-49f7-4ff4-9df2-98eda3e77d04

📥 Commits

Reviewing files that changed from the base of the PR and between 94be2a4 and 5252f54.

📒 Files selected for processing (2)
  • src/js/thirdparty/undici.js
  • test/js/first_party/undici/undici.test.ts

Comment thread src/js/thirdparty/undici.js Outdated

@claude claude 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.

Both earlier nits are addressed and I didn't find anything new, but deferring to a maintainer: the exported WebSocket changes from the native class to a subclass (previously require('undici').WebSocket === globalThis.WebSocket), and with #34635 / #36102 / #30561 all open against the same surface, the sequencing — and whether landing this alongside #34635 would double-publish on the ping/pong channels — is a call for a human.

What was reviewed:

  • ping() argument validation and OPEN-gated send against undici 7.x semantics; controlFramePayload coercion for the non-nodebuffer binaryType path.
  • Tests: failure events wired to reject, per-socket channel filtering under describe.concurrent, readyState precondition asserted before the no-op path.
  • Checked that $isTypedArrayView + captured NativeArrayBuffer match the tamper-resistance pattern in ws.js.
Extended reasoning...

Overview

The PR extends src/js/thirdparty/undici.js (~50 lines) and adds ~140 lines of tests to test/js/first_party/undici/undici.test.ts. It exports a module-level ping(ws, payload) and replaces the exported WebSocket with a subclass of the native class whose constructor attaches ping/pong event listeners that republish to the undici:websocket:ping/undici:websocket:pong diagnostics channels. No native code is touched. All prior review feedback on this PR — my two nits (Buffer coercion for non-default binaryType, wiring failure events to reject in the ping test), CodeRabbit's items (readyState precondition, error/close rejection, extra validation variants, captured ArrayBuffer intrinsics), and the comment-cop JSDoc note — has been applied and the threads are resolved.

Security risks

None identified. This is a JS-only compat shim; ping() validates its inputs (WebSocket instance check, Buffer-only payload, ≤125 bytes) before delegating to the native ws.ping(), and the diagnostics-channel publish is guarded by hasSubscribers. No auth, crypto, filesystem, or network-origin decisions are involved.

Level of scrutiny

Medium. The runtime change is small and well-tested (fails on main, passes on the PR under both debug/ASAN and release), but it does alter user-visible API surface: require('undici').WebSocket was previously the native WebSocket class itself and is now a subclass. instanceof globalThis.WebSocket still holds and the motivating consumer (@slack/socket-mode) works, but strict identity checks against globalThis.WebSocket would now differ, which is the kind of compat-surface tweak a maintainer should sign off on.

Other factors

Three open PRs overlap this surface: #34635 publishes these same channels natively in WebSocket.cpp, #36102 resolves bare undici to the installed package, and #30561 removes the shim entirely. The author's rationale for this PR standing alone is reasonable, but the description's claim that #34635 "supersedes" the subclass listeners isn't automatic — if both land as-is, each ping/pong would publish twice (once natively, once from the subclass listener). That coordination, plus the broader question of which direction to take the undici shim, is a maintainer decision rather than something to auto-approve.

@robobun

robobun commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

On the coordination point: agreed that the supersede is not automatic. If #34635 lands after this, its change should drop the two subclass listeners here (they are self-contained in the WebSocket constructor in src/js/thirdparty/undici.js); if this lands after #34635, I will remove them from this diff before merge. The ping() export is orthogonal and needed either way.

On identity: require('undici').WebSocket is now a subclass rather than globalThis.WebSocket itself, so instanceof globalThis.WebSocket still holds but strict equality with the global does not. Node has the same non-identity in reverse for code that patches one and expects the other; happy to adjust if a maintainer prefers attaching listeners differently.

@sadfun

sadfun commented Aug 13, 2026

Copy link
Copy Markdown

@robobun any plans to merge this? 👀

@robobun

robobun commented Aug 21, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 1:02 PM PT - Aug 21st, 2026

✅ @robobun, your commit 139022da2b16fd437150fc4b1324729cdb83064a passed in Build #102797! 🎉


🧪   To try this PR locally:

bunx bun-pr 37111

That installs a local version of the PR into your bun-37111 executable, so you can run:

bun-37111 --bun

@claude claude 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.

I reviewed this PR and didn't find any bugs; my two earlier inline nits (Buffer coercion for non-default binaryType, and wiring failure events in the ping test) have both been addressed. Deferring the approval to a maintainer because the exported WebSocket is now a subclass rather than the native class itself — the author flagged that identity change for maintainer input — and because this overlaps with #34635 / #36102 / #30561, which need a human to sequence.

What was reviewed:

  • ping() argument validation and no-op-while-not-OPEN semantics against undici 7.x behavior.
  • controlFramePayload coercion path for nodebuffer/arraybuffer/typed-array inputs; captured ArrayBuffer + $isTypedArrayView for tamper-resistance.
  • Test hermeticity: per-socket channel filtering under describe.concurrent, error/close wired to reject the awaited resolvers.
Extended reasoning...

Overview

The PR adds two things to the built-in undici shim (src/js/thirdparty/undici.js): a module-level ping(ws, payload) export mirroring undici v7's, and a WebSocket subclass of the native class that republishes Bun's native ping/pong events onto the undici:websocket:ping/undici:websocket:pong diagnostics channels. ~50 lines of shim JS plus ~140 lines of tests in test/js/first_party/undici/undici.test.ts. No native code touched.

Security risks

None identified. This is a thin JS shim over existing native WebSocket behavior; there is no auth, crypto, filesystem, or untrusted parsing surface. The 125-byte payload cap is validated before delegating to native ws.ping().

Level of scrutiny

Moderate. The logic is small and well-tested, but it changes user-facing API surface: require('undici').WebSocket was previously identical to the native WebSocket constructor and is now a subclass (so === globalThis.WebSocket becomes false, and every undici-constructed socket now carries two extra event listeners). The author explicitly flagged this identity change for maintainer input in the thread. That is the kind of API-shape call a human should sign off on rather than a bot.

Other factors

Three open PRs touch the same surface with different strategies — #34635 publishes these channels natively in WebSocket.cpp, #36102 resolves bare undici to the installed package, and #30561 deletes the shim outright. If #34635 lands alongside this, the channels would double-publish until one side is removed; the author acknowledged the coordination burden. All prior CodeRabbit and claude[bot] inline comments are resolved, tests pass on both debug/ASAN and release per the PR evidence, and a community user is asking for merge — so the remaining gate is a maintainer decision on the subclass approach and PR sequencing, not code correctness.

…ics channels

The builtin undici shim did not export ping() and its WebSocket never
published the undici:websocket:ping/pong diagnostics channels, so
@slack/socket-mode heartbeats threw (ping is not a function) and its
health monitor counted every pong as missed, producing a permanent
reconnect loop.

ping(ws, payload) mirrors undici's validation (Buffer-or-undefined
payload, 125-byte cap, no-op unless open) and sends a ping frame via
the native WebSocket. The shim's WebSocket is now a subclass that
publishes {payload, websocket} to the ping/pong channels when frames
arrive, matching undici's message shape.

Fixes #37110
Run the suite concurrently, filter diagnostics-channel messages by
websocket identity, reject on error/close instead of hanging, pin the
CONNECTING precondition for the no-op case, and cover null/Uint8Array
payloads and undefined/null receivers.
@robobun
robobun force-pushed the farm/7076b23d/undici-websocket-ping branch from 07d6a6c to 139022d Compare August 21, 2026 19:43

@claude claude 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.

I reviewed this PR and didn't find any bugs — my earlier inline comments (Buffer coercion for non-default binaryType, wiring failure events to reject) have both been addressed. A maintainer look would still be worthwhile for the two design points the author flagged: the exported WebSocket is now a subclass (so require('undici').WebSocket !== globalThis.WebSocket), and this overlaps with #34635/#36102/#30561 which take different approaches to the same surface.

What was reviewed:

  • ping() argument validation and OPEN-state gating against undici 7.x semantics — matches (Buffer-only payload, 125-byte cap, no-op when not OPEN).
  • controlFramePayload coercion — captured ArrayBuffer + $isTypedArrayView intrinsic, covered by the binaryType='arraybuffer' test.
  • Test hygiene — describe.concurrent with per-socket channel filtering, error/close wired to reject, readyState asserted before the no-op path.
Extended reasoning...

Overview

The PR adds two things to Bun's built-in undici shim (src/js/thirdparty/undici.js): a module-level ping(ws, payload) export matching undici v7's, and a WebSocket subclass whose instances publish { payload, websocket } on the undici:websocket:ping/undici:websocket:pong diagnostics channels. It fixes #37110 (@slack/socket-mode reconnect loop). ~50 lines of shim JS plus ~140 lines of new tests in test/js/first_party/undici/undici.test.ts. No native changes.

Security risks

None identified. Shim-only JS, no new parsing of untrusted input beyond the existing native WebSocket.ping() path (which already enforces the 125-byte cap independently). The tamper-resistance nit (mutable ArrayBuffer global) was addressed with a module-captured reference and the $isTypedArrayView intrinsic.

Level of scrutiny

Medium. The code is small and additive, tests pass on debug+ASAN and release, and all prior automated feedback (CodeRabbit and my own inline comments) has been addressed and marked resolved. What keeps this from being a rubber-stamp is that it makes a user-visible API-shape decision — the exported WebSocket becomes a subclass of the native one rather than the native class itself — and the author explicitly said they're "happy to adjust if a maintainer prefers attaching listeners differently."

Other factors

Three open PRs address the same failure class differently: #34635 publishes the websocket diagnostics channels natively in WebSocket.cpp, #36102 resolves bare undici to the installed npm package with the shim as fallback, and #30561 removes the shim entirely. The author's position (this is the minimal self-contained fix that stands alone; ping() is needed regardless; the subclass listeners are trivially removable if #34635 lands) is reasonable, but choosing which of these lands and in what order is a maintainer call. The author's own comment on 2026-08-07 explicitly invites that input on both the coordination and the subclass-vs-alternative question, so deferring rather than approving.

@robobun

robobun commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator Author

A second report of this came in (Bolt 5 / @slack/socket-mode 3.x reconnecting every few seconds under Bun 1.3.9, 1.3.13 and 1.4.2). I checked this PR against @slack/socket-mode@3.0.1 itself, not only the unit tests here.

The script below drives SlackWebSocket from @slack/socket-mode/dist/src/SlackWebSocket.js against a local Bun.serve websocket server with clientPingTimeoutMS: 900, for 5 seconds:

  • Bun 1.4.3: undici.ping is undefined, then Failed to send ping to Slack (error: TypeError: (0, undici_1.ping) is not a function ...), close frame, >>> CLOSED. This repeats on every reconnect.
  • This patch applied to current main (85e9ddc, applies clean): undici.ping is function, 16 pings sent, 16 pongs received through undici:websocket:pong, 0 errors, 0 closes. Same output as Node 26.3.0 with real undici 7.29.1.
repro.mjs
// bun add @slack/socket-mode@3.0.1 undici@7 ws
import { createRequire } from "node:module";
import { EventEmitter } from "node:events";
const require = createRequire(import.meta.url);
console.log("undici.ping is", typeof require("undici").ping);
const { SlackWebSocket } = require("@slack/socket-mode/dist/src/SlackWebSocket.js");
const server = globalThis.Bun
  ? Bun.serve({ port: 0, fetch: (req, srv) => (srv.upgrade(req) ? undefined : new Response("no", { status: 400 })), websocket: { message() {} } })
  : await new Promise(async (resolve) => { const { WebSocketServer } = await import("ws"); const wss = new WebSocketServer({ port: 0 }, () => resolve({ port: wss.address().port })); });
const log = (lvl) => (...m) => console.log(lvl, ...m);
const logger = { debug: log("debug"), info: log("info"), warn: log("warn"), error: log("error"), setLevel() {}, getLevel: () => "debug", setName() {} };
const client = new EventEmitter();
client.on("close", () => console.log(">>> CLOSED (the reconnect churn)"));
const ws = new SlackWebSocket({ url: `ws://127.0.0.1:${server.port}/`, client, logger, logLevel: "debug", pingPongLoggingEnabled: true, clientPingTimeoutMS: 900, serverPingTimeoutMS: 30000 });
await ws.connect();
setTimeout(() => process.exit(0), 5000);

@steipete steipete left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Confirmed against OpenClaw's real Slack provider interop lane. Shipping Bun 1.4.2 fails with ping is not a function; applying this PR's final implementation to current Bun main makes the complete 19-test Slack provider lifecycle file pass under an ASan debug build. The complete Bun undici test file also passes (26/26).

AI-assisted validation: Codex was used to trace the OpenClaw failure, inspect the Bun and Undici contracts, build Bun, and run the tests; I reviewed the implementation and results.

@robobun

robobun commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the independent validation against a real Slack Socket Mode lifecycle. That covers the end-to-end path the unit tests cannot reach (an actual Slack peer answering client pings), so it is useful evidence for the reviewer.

steipete added a commit to steipete/bun that referenced this pull request Sep 17, 2026
Ports oven-sh#37111 into the OpenClaw compatibility build.
steipete added a commit to openclaw/bun that referenced this pull request Sep 30, 2026
Ports oven-sh#37111 into the OpenClaw compatibility build.

(cherry picked from commit 278105f)
steipete added a commit to openclaw/bun that referenced this pull request Sep 30, 2026
Ports oven-sh#37111 into the OpenClaw compatibility build.

(cherry picked from commit 278105f)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

undici shim lacks ping() export and undici:websocket:ping/pong diagnostics channels, breaking @slack/socket-mode (permanent reconnect loop)

3 participants