Skip to content

fix(bluebubbles): route fetchImpl through bundled undici fetch on Node 24+#66103

Closed
coletebou wants to merge 1 commit intoopenclaw:mainfrom
coletebou:fix/bb-runtime-dispatcher-v2
Closed

fix(bluebubbles): route fetchImpl through bundled undici fetch on Node 24+#66103
coletebou wants to merge 1 commit intoopenclaw:mainfrom
coletebou:fix/bb-runtime-dispatcher-v2

Conversation

@coletebou
Copy link
Copy Markdown
Contributor

Summary

BlueBubbles attachment downloads fail on Node 24+ because the SSRF guard's pinned-DNS dispatcher is an undici 8.x Agent, but Node 24's built-in globalThis.fetch uses undici 7.x internally. Passing the 8.x dispatcher causes "invalid onRequestStart method" errors.

This routes through fetchWithRuntimeDispatcher (bundled undici's own fetch) when a dispatcher is present in init, matching the approach already used by the Slack extension (#62239). This preserves SSRF DNS pinning instead of stripping the dispatcher.

Supersedes #63984 which took a less clean approach (stripping the dispatcher entirely, losing DNS pinning).

Changes

  • extensions/bluebubbles/src/attachments.ts: Use fetchWithRuntimeDispatcher when init contains a dispatcher, fall back to globalThis.fetch otherwise. Remove unused blueBubblesFetchWithTimeout import.

Test plan

  • Send an image to a BlueBubbles-connected agent on Node 24+ and confirm the attachment downloads successfully
  • Verify no SSRF guard regressions (DNS pinning preserved)

…hment downloads on Node 24+

The SSRF guard pinned-DNS dispatcher is an undici 8.x Agent, but Node 24
built-in globalThis.fetch uses undici 7.x internally. Passing the 8.x
dispatcher to the native fetch causes 'invalid onRequestStart method'
errors, breaking all BlueBubbles attachment downloads.

Route through fetchWithRuntimeDispatcher (bundled undici own fetch) when
a dispatcher is present, matching the approach used by the Slack extension
(openclaw#62239). This preserves SSRF DNS pinning instead of stripping the
dispatcher entirely.
@openclaw-barnacle openclaw-barnacle Bot added channel: bluebubbles Channel integration: bluebubbles size: XS labels Apr 13, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 44561f209d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +129 to +132
return await useFetch(
resolveRequestUrl(input),
{ ...init, method: init?.method ?? "GET" },
opts.timeoutMs,
),
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Restore timeout support for attachment downloads

downloadBlueBubblesAttachment previously used blueBubblesFetchWithTimeout, which enforced opts.timeoutMs (and a 10s default), but this new fetchImpl now calls fetchWithRuntimeDispatcher/globalThis.fetch directly with no timeout path. When the BlueBubbles host stalls (e.g., hung connection or non-responsive server), attachment fetches can now wait indefinitely and block message processing, so this change regresses reliability for slow/failing networks.

Useful? React with 👍 / 👎.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 13, 2026

Greptile Summary

Routes fetchImpl through fetchWithRuntimeDispatcher (bundled undici's own fetch) when a pinned-DNS dispatcher is present in init, fixing the "invalid onRequestStart method" crash on Node 24+ and preserving SSRF DNS pinning. The approach mirrors the Slack extension's fix.

One P1 concern: opts.timeoutMs (defaulting to 10 s in blueBubblesFetchWithTimeout) is no longer forwarded to the new fetch path, so attachment downloads have no timeout and can hang indefinitely on stalled servers.

Confidence Score: 3/5

Fix is directionally correct but drops an existing 10 s timeout guard, leaving downloads able to hang indefinitely.

The Node 24+ routing fix is sound and follows the established Slack pattern. However, opts.timeoutMs (previously forwarded via blueBubblesFetchWithTimeout) is now silently ignored — a real behavioral regression on all Node versions where the dispatcher path is taken. That P1 issue should be addressed before merging.

extensions/bluebubbles/src/attachments.ts — the fetchImpl block at lines 125–133

Fix All in Codex Fix All in Claude Code

Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/bluebubbles/src/attachments.ts
Line: 125-133

Comment:
**`opts.timeoutMs` is now silently dropped**

The previous `fetchImpl` passed `opts.timeoutMs` (default 10 s) to `blueBubblesFetchWithTimeout`, which created an `AbortController` and aborted the request on expiry. The new implementation calls `fetchWithRuntimeDispatcher` / `globalThis.fetch` with no timeout and no signal, so a stalled BlueBubbles server will cause the download to hang indefinitely. Any caller that passes a `timeoutMs` in `BlueBubblesAttachmentOpts` will find it has no effect.

A minimal fix is to forward a timeout signal (using the already-available `AbortSignal.timeout` in Node 17.3+), falling back to `opts.timeoutMs ?? 10_000`:

```typescript
fetchImpl: async (input, init) => {
  const useFetch = init && "dispatcher" in init
    ? fetchWithRuntimeDispatcher
    : globalThis.fetch;
  const resolvedInit: RequestInit = {
    ...init,
    method: init?.method ?? "GET",
    ...(!(init as RequestInit | undefined)?.signal
      ? { signal: AbortSignal.timeout(opts.timeoutMs ?? 10_000) }
      : {}),
  };
  return await useFetch(resolveRequestUrl(input), resolvedInit);
},
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/bluebubbles/src/attachments.ts
Line: 126-128

Comment:
**Missing `isMockedFetch` guard (parity with Slack pattern)**

The Slack reference implementation (`extensions/slack/src/monitor/media.ts`) uses `!isMockedFetch(globalThis.fetch)` before routing to `fetchWithRuntimeDispatcher`, so that a test-installed mock on `globalThis.fetch` is honoured even when `init` carries a `dispatcher`. The BlueBubbles code omits this guard and will always call `fetchWithRuntimeDispatcher` when a dispatcher is present, bypassing any `globalThis.fetch` mock. Current tests are insulated because they fully replace `fetchRemoteMedia`, but future tests using the real `fetchRemoteMedia` with a `globalThis.fetch` mock would silently execute real network calls instead of the mock.

```suggestion
        const useFetch = init && "dispatcher" in init && !isMockedFetch(globalThis.fetch)
          ? fetchWithRuntimeDispatcher
          : globalThis.fetch;
```
(requires a local `isMockedFetch` helper, mirroring the one in `extensions/slack/src/monitor/media.ts`)

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(bluebubbles): route fetchImpl throug..." | Re-trigger Greptile

Comment on lines +125 to +133
fetchImpl: async (input, init) => {
const useFetch = init && "dispatcher" in init
? fetchWithRuntimeDispatcher
: globalThis.fetch;
return await useFetch(
resolveRequestUrl(input),
{ ...init, method: init?.method ?? "GET" },
opts.timeoutMs,
),
);
},
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.

P1 opts.timeoutMs is now silently dropped

The previous fetchImpl passed opts.timeoutMs (default 10 s) to blueBubblesFetchWithTimeout, which created an AbortController and aborted the request on expiry. The new implementation calls fetchWithRuntimeDispatcher / globalThis.fetch with no timeout and no signal, so a stalled BlueBubbles server will cause the download to hang indefinitely. Any caller that passes a timeoutMs in BlueBubblesAttachmentOpts will find it has no effect.

A minimal fix is to forward a timeout signal (using the already-available AbortSignal.timeout in Node 17.3+), falling back to opts.timeoutMs ?? 10_000:

fetchImpl: async (input, init) => {
  const useFetch = init && "dispatcher" in init
    ? fetchWithRuntimeDispatcher
    : globalThis.fetch;
  const resolvedInit: RequestInit = {
    ...init,
    method: init?.method ?? "GET",
    ...(!(init as RequestInit | undefined)?.signal
      ? { signal: AbortSignal.timeout(opts.timeoutMs ?? 10_000) }
      : {}),
  };
  return await useFetch(resolveRequestUrl(input), resolvedInit);
},
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/bluebubbles/src/attachments.ts
Line: 125-133

Comment:
**`opts.timeoutMs` is now silently dropped**

The previous `fetchImpl` passed `opts.timeoutMs` (default 10 s) to `blueBubblesFetchWithTimeout`, which created an `AbortController` and aborted the request on expiry. The new implementation calls `fetchWithRuntimeDispatcher` / `globalThis.fetch` with no timeout and no signal, so a stalled BlueBubbles server will cause the download to hang indefinitely. Any caller that passes a `timeoutMs` in `BlueBubblesAttachmentOpts` will find it has no effect.

A minimal fix is to forward a timeout signal (using the already-available `AbortSignal.timeout` in Node 17.3+), falling back to `opts.timeoutMs ?? 10_000`:

```typescript
fetchImpl: async (input, init) => {
  const useFetch = init && "dispatcher" in init
    ? fetchWithRuntimeDispatcher
    : globalThis.fetch;
  const resolvedInit: RequestInit = {
    ...init,
    method: init?.method ?? "GET",
    ...(!(init as RequestInit | undefined)?.signal
      ? { signal: AbortSignal.timeout(opts.timeoutMs ?? 10_000) }
      : {}),
  };
  return await useFetch(resolveRequestUrl(input), resolvedInit);
},
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex Fix in Claude Code

Comment on lines +126 to +128
const useFetch = init && "dispatcher" in init
? fetchWithRuntimeDispatcher
: globalThis.fetch;
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.

P2 Missing isMockedFetch guard (parity with Slack pattern)

The Slack reference implementation (extensions/slack/src/monitor/media.ts) uses !isMockedFetch(globalThis.fetch) before routing to fetchWithRuntimeDispatcher, so that a test-installed mock on globalThis.fetch is honoured even when init carries a dispatcher. The BlueBubbles code omits this guard and will always call fetchWithRuntimeDispatcher when a dispatcher is present, bypassing any globalThis.fetch mock. Current tests are insulated because they fully replace fetchRemoteMedia, but future tests using the real fetchRemoteMedia with a globalThis.fetch mock would silently execute real network calls instead of the mock.

Suggested change
const useFetch = init && "dispatcher" in init
? fetchWithRuntimeDispatcher
: globalThis.fetch;
const useFetch = init && "dispatcher" in init && !isMockedFetch(globalThis.fetch)
? fetchWithRuntimeDispatcher
: globalThis.fetch;

(requires a local isMockedFetch helper, mirroring the one in extensions/slack/src/monitor/media.ts)

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/bluebubbles/src/attachments.ts
Line: 126-128

Comment:
**Missing `isMockedFetch` guard (parity with Slack pattern)**

The Slack reference implementation (`extensions/slack/src/monitor/media.ts`) uses `!isMockedFetch(globalThis.fetch)` before routing to `fetchWithRuntimeDispatcher`, so that a test-installed mock on `globalThis.fetch` is honoured even when `init` carries a `dispatcher`. The BlueBubbles code omits this guard and will always call `fetchWithRuntimeDispatcher` when a dispatcher is present, bypassing any `globalThis.fetch` mock. Current tests are insulated because they fully replace `fetchRemoteMedia`, but future tests using the real `fetchRemoteMedia` with a `globalThis.fetch` mock would silently execute real network calls instead of the mock.

```suggestion
        const useFetch = init && "dispatcher" in init && !isMockedFetch(globalThis.fetch)
          ? fetchWithRuntimeDispatcher
          : globalThis.fetch;
```
(requires a local `isMockedFetch` helper, mirroring the one in `extensions/slack/src/monitor/media.ts`)

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex Fix in Claude Code

@coletebou
Copy link
Copy Markdown
Contributor Author

Closing — review caught a dropped timeout regression and missing isMockedFetch guard. Resubmitting with fixes.

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

Labels

channel: bluebubbles Channel integration: bluebubbles size: XS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant