openclaw - ✅(Solved) Fix [Fix] Inbound image attachments silently fail in BlueBubbles and Slack — SSRF guard dispatcher incompatible with Node native fetch [1 pull requests, 2 comments, 3 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#62530Fetched 2026-04-08 03:02:57
View on GitHub
Comments
2
Participants
3
Timeline
4
Reactions
0
Timeline (top)
commented ×2mentioned ×1subscribed ×1

Error Message

Agent receives messages with image attachments but never saves them to ~/.openclaw/media/inbound/. No visible error. Agent replies saying it can't see the image. This affects both BlueBubbles and Slack on macOS, OpenClaw v2026.4.5. if (!response.ok) throw new Error(HTTP ${response.status});

Root Cause

The fetchRemoteMedia helper creates an undici dispatcher via the SSRF guard and passes it as part of init to the fetchImpl callback. When that reaches Node's native fetch(), it throws:

fetch failed | invalid onRequestStart method

This is silently swallowed by the surrounding try/catch. Nothing is logged at default log levels.

Fix Action

Fix

Replace fetchRemoteMedia + SSRF guard with plain fetch() + AbortController in two places:

PR fix notes

PR #67510: fix(bluebubbles): restore inbound image attachments and accept updated-message events

Description (problem / solution / changelog)

Summary

Four interconnected fixes for BlueBubbles inbound media that together restore image attachment handling on Node 22+:

  1. Strip bundled-undici dispatcher from non-SSRF fetch pathblueBubblesFetchWithTimeout's non-SSRF fallback was spreading a bundled-undici dispatcher into globalThis.fetch(), which Node 22's built-in undici rejects with TypeError: invalid onRequestStart method. This silently broke ALL inbound attachment downloads. (#64105, #61861, #67241, #62530)

  2. Accept updated-message events carrying attachments — BlueBubbles fires updated-message when attachments are indexed after the initial new-message (which may arrive with attachments: []). The webhook handler was filtering these out as non-reaction events. (#65430)

  3. Event-type-aware dedup key — the persistent GUID dedup now suffixes updated-message keys with :updated so follow-up attachment events aren't rejected as duplicates of the already-committed new-message. (Relates to #52277)

  4. Retry attachment fetch for empty arrays — when the initial webhook arrives with empty attachments (image-only messages or updated-message events), waits 2s and re-fetches from the BB API as a fallback. (Relates to #67437)

Test plan

  • pnpm tsgo — no new type errors
  • pnpm test extensions/bluebubbles/ — 434 tests pass (9 new tests added)
  • Manual: send standalone image via iMessage → agent receives and processes it
  • Manual: send text + image together → agent receives both
  • pnpm build — verify no [INEFFECTIVE_DYNAMIC_IMPORT] warnings

Issues closed

Closes #64105, closes #61861, closes #65430, closes #67241, closes #62530.

Related issues (not fixed by this PR)

  • #34749 — image attachments blocked by SSRF guard (localhost URL) — partially addressed by the dispatcher fix, but the broader SSRF allowlist for localhost BB servers is a separate concern
  • #57181 — SSRF guard blocks BB plugin internal API calls to private IP — same broader SSRF allowlist issue
  • #59722 — SSRF allowlist doesn't cover reactions — separate SSRF scope gap
  • #60715 — BB health check fails on LAN/private serverUrl — separate SSRF/health-check issue

Superseded PRs

  • #66120 — accept updated-message events carrying attachments (fully subsumed by fix #2)
  • #67437 — retry attachment fetch when webhook arrives with empty array (fully subsumed by fix #4)
  • #66108 — route fetchImpl through bundled undici fetch on Node 24+ (different approach to same dispatcher issue, subsumed by fix #1)

Related PRs

  • #52277 — dedupe webhook replays without dropping edits (assigned to @omarshahine, complementary finer-grained edit dedup)

🤖 Generated with Claude Code

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • extensions/bluebubbles/src/attachments.test.ts (modified, +88/-1)
  • extensions/bluebubbles/src/attachments.ts (modified, +52/-2)
  • extensions/bluebubbles/src/inbound-dedupe.test.ts (modified, +36/-0)
  • extensions/bluebubbles/src/inbound-dedupe.ts (modified, +15/-3)
  • extensions/bluebubbles/src/monitor-normalize.ts (modified, +5/-1)
  • extensions/bluebubbles/src/monitor-processing.ts (modified, +50/-6)
  • extensions/bluebubbles/src/monitor.ts (modified, +13/-3)
  • extensions/bluebubbles/src/types.ts (modified, +9/-1)
  • scripts/check-no-raw-channel-fetch.mjs (modified, +1/-1)

Code Example

fetch failed | invalid onRequestStart method

---

const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 30000);
try {
  const response = await fetch(url, { method: 'GET', signal: controller.signal });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  const buf = await response.arrayBuffer();
  return { buffer: new Uint8Array(buf), contentType: response.headers.get('content-type') ?? attachment.mimeType };
} finally { clearTimeout(timer); }

---

const response = await fetch(url, {
  method: 'GET',
  headers: { Authorization: `Bearer ${params.token}` },
  redirect: 'follow',
  signal: controller.signal
});

---

const downloaded = await downloadBlueBubblesAttachment(attachment, {
  cfg: config,
  accountId: account.accountId,
  maxBytes,
  allowPrivateNetwork: isPrivateNetworkOptInEnabled(account.config)  // ← add this
});
RAW_BUFFERClick to expand / collapse

Sharing a fix for others hitting silent image download failures in BlueBubbles (iMessage) and Slack channels. Took a full day of debugging to find — posting here so others don't have to.

Symptom

Agent receives messages with image attachments but never saves them to ~/.openclaw/media/inbound/. No visible error. Agent replies saying it can't see the image. This affects both BlueBubbles and Slack on macOS, OpenClaw v2026.4.5.

Root Cause

The fetchRemoteMedia helper creates an undici dispatcher via the SSRF guard and passes it as part of init to the fetchImpl callback. When that reaches Node's native fetch(), it throws:

fetch failed | invalid onRequestStart method

This is silently swallowed by the surrounding try/catch. Nothing is logged at default log levels.

Fix

Replace fetchRemoteMedia + SSRF guard with plain fetch() + AbortController in two places:

1. BlueBubbles (reactions-*.jsdownloadBlueBubblesAttachment)

const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 30000);
try {
  const response = await fetch(url, { method: 'GET', signal: controller.signal });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  const buf = await response.arrayBuffer();
  return { buffer: new Uint8Array(buf), contentType: response.headers.get('content-type') ?? attachment.mimeType };
} finally { clearTimeout(timer); }

Safe because dangerouslyAllowPrivateNetwork: true is already in config for local network access.

2. Slack (actions-*.jsresolveSlackMedia)

Same pattern but add the Bearer token header:

const response = await fetch(url, {
  method: 'GET',
  headers: { Authorization: `Bearer ${params.token}` },
  redirect: 'follow',
  signal: controller.signal
});

3. Also required for BlueBubbles (channel.runtime-*.js)

Add allowPrivateNetwork to the downloadBlueBubblesAttachment call opts — it was being silently dropped:

const downloaded = await downloadBlueBubblesAttachment(attachment, {
  cfg: config,
  accountId: account.accountId,
  maxBytes,
  allowPrivateNetwork: isPrivateNetworkOptInEnabled(account.config)  // ← add this
});

Verified

Both iMessage (BlueBubbles, Private API disabled) and Slack image attachments now download correctly and are described by the agent.

Related

Original bug report with full diagnosis trail: #62248

extent analysis

TL;DR

Replace the fetchRemoteMedia helper with a plain fetch() call and add an AbortController to fix silent image download failures in BlueBubbles and Slack channels.

Guidance

  • Identify the affected code paths in reactions-*.js and actions-*.js and replace the fetchRemoteMedia helper with the suggested fetch() implementation.
  • Add the allowPrivateNetwork option to the downloadBlueBubblesAttachment call in channel.runtime-*.js to ensure private network access is properly configured.
  • Verify that the dangerouslyAllowPrivateNetwork config option is set to true to allow local network access.
  • Test the changes with both BlueBubbles and Slack channels to ensure image attachments are downloaded correctly.

Example

The provided code snippets in the issue body demonstrate the required changes, including the use of AbortController and fetch() with the necessary headers and options.

Notes

The fix assumes that the dangerouslyAllowPrivateNetwork config option is already set to true, which is a prerequisite for the suggested changes to work. Additionally, the changes should be verified to ensure they do not introduce any new issues or security vulnerabilities.

Recommendation

Apply the workaround by replacing the fetchRemoteMedia helper with the suggested fetch() implementation and adding the required options to ensure private network access is properly configured. This should fix the silent image download failures in BlueBubbles and Slack channels.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING