Skip to content

BlueBubbles attachment downloads silently fail on Node.js 22 (undici dispatcher mismatch) #64105

@mpalermiti

Description

@mpalermiti

Summary

Inbound BlueBubbles image attachments silently fail to download. No files appear in ~/.openclaw/media/inbound/. The agent receives the message text/placeholder but cannot see the image.

Environment

  • OpenClaw 2026.4.9
  • Node.js 22.22.2
  • macOS 26.4.0
  • BlueBubbles server on http://127.0.0.1:1234
  • Config: channels.bluebubbles.network.dangerouslyAllowPrivateNetwork: true

Root cause

fetchWithSsrFGuard creates a pinned DNS dispatcher using OpenClaw's bundled undici and passes it via init.dispatcher to the custom fetchImpl in downloadBlueBubblesAttachment. That fetchImpl delegates to blueBubblesFetchWithTimeout (the non-ssrfPolicy branch at the bottom of the function), which calls globalThis.fetch() — backed by Node.js 22's built-in undici.

Node's built-in undici rejects the foreign dispatcher:

TypeError: fetch failed
  cause: invalid onRequestStart method (UND_ERR_INVALID_ARG)

The error is swallowed by the try/catch in processMessage (channel.runtime) and only logged at verbose level, making it appear as though attachments simply aren't present.

Affected code

In the source for blueBubblesFetchWithTimeout (likely extensions/bluebubbles/src/types.ts), the else branch that runs when ssrfPolicy is undefined:

// ssrfPolicy is undefined when called from the fetchImpl wrapper
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
    return await fetch(url, {
        ...init,       // ← init.dispatcher is from bundled undici
        signal: controller.signal,
    });
} finally {
    clearTimeout(timer);
}

Fix

Strip dispatcher from init before passing to the global fetch(). The SSRF validation has already passed in the outer fetchWithSsrFGuard call, so the dispatcher is not needed here:

const { dispatcher: _d, ...safeInit } = init ?? {};
return await fetch(url, {
    ...safeInit,
    signal: controller.signal,
});

Broader note

The supportsDispatcherInit heuristic in fetchWithSsrFGuard assumes that any non-global custom fetchImpl supports the bundled undici dispatcher. But downloadBlueBubblesAttachment's fetchImpl delegates to globalThis.fetch, which uses a different undici version. Any other custom fetchImpl that does the same will hit this incompatibility.

An alternative fix at the fetchWithSsrFGuard level would be to use fetchWithRuntimeDispatcher (which uses the bundled undici fetch directly) whenever there is a dispatcher, regardless of whether a custom fetchImpl is provided.

Reproduction

  1. Configure BlueBubbles with a local server URL (e.g., http://127.0.0.1:1234)
  2. Set channels.bluebubbles.network.dangerouslyAllowPrivateNetwork: true
  3. Send an image via iMessage to a chat monitored by OpenClaw
  4. Observe that ~/.openclaw/media/inbound/ remains empty
  5. Enable verbose logging to see the TypeError in the attachment download catch block

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions