From 77d9fd693f189d65b4317e826544d902de932b9e Mon Sep 17 00:00:00 2001 From: Omar Shahine Date: Thu, 16 Apr 2026 10:04:20 -0700 Subject: [PATCH] fix(bluebubbles): restore inbound image attachments and accept updated-message events (#67510) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(bluebubbles): restore inbound image attachments and accept updated-message events Four interconnected fixes for BlueBubbles inbound media: 1. Strip bundled-undici dispatcher from non-SSRF fetch path so attachment downloads no longer silently fail on Node 22+ (#64105, #61861) 2. Accept updated-message webhook events that carry attachments instead of filtering them as non-reaction events (#65430) 3. Include eventType in the persistent GUID dedup key so updated-message follow-ups are not rejected as duplicates of the original new-message (#52277) 4. Retry attachment fetch from BB API (2s delay) when the initial webhook arrives with an empty attachments array — image-only messages and updated-message events only (#67437) Closes #64105, closes #61861, closes #65430. * fix(bluebubbles): resolve review findings — SSRF policy, reuse extractAttachments, add tests - F1 (BLOCKER): pass undefined instead of {} for SSRF policy when allowPrivateNetwork is false, so localhost BB servers are not blocked. - F2 (IMPORTANT): reuse exported extractAttachments() from monitor-normalize instead of duplicating field extraction logic. - F3 (IMPORTANT): simplify asRecord(asRecord(payload)?.data) to asRecord(payload.data) since payload is already Record. - F4 (NIT): bind retryMessageId before the guard to eliminate non-null assertion. - F5 (IMPORTANT): add 4 tests for fetchBlueBubblesMessageAttachments covering success, non-ok HTTP, empty data, and guid-less entries. - Add CHANGELOG entry for the user-facing fix. * fix(ci): update raw-fetch allowlist line number after dispatcher strip * fix(bluebubbles): resolve PR review findings (#67510) - monitor-processing: move attachment retry into the !rawBody guard so image-only new-message events that arrive with empty attachments and empty text are recovered via a BB API refetch before being dropped. The existing retry block at the end of processMessageAfterDedupe was unreachable for this case because the !rawBody early-return fired first. (Greptile) - monitor: derive isAttachmentUpdate from the normalized message shape instead of raw payload.data.attachments so updated-message webhooks with attachments under wrapper formats (payload.message, JSON-string payloads) are correctly routed through for processing instead of silently filtered. (Codex) - types: use bundled-undici fetch when init.dispatcher is present so the SSRF guard's DNS-pinning dispatcher is preserved when this function is called as fetchImpl from guarded callers (e.g. the attachment download path via fetchRemoteMedia). Falls back to globalThis.fetch when no dispatcher is present so tests that stub globalThis.fetch keep working. (Codex) - attachments: blueBubblesPolicy returns undefined for the non-private case (matching monitor-processing's helper) so sendBlueBubblesAttachment stops routing localhost BB through the SSRF guard. (Greptile) - scripts/check-no-raw-channel-fetch: bump the types.ts allowlist line to match the restructured non-SSRF branch. * fix(bluebubbles): move attachment retry before rawBody guard, fix stale log Move the attachment retry block (2s BB API refetch for empty attachments) before the !rawBody early-return guard. Previously, image-only messages with text='' and attachments=[] would be dropped by the !rawBody check before the retry could fire, making fix #4 dead code for its primary use-case. Now the retry runs first and recomputes the placeholder from resolved attachments so rawBody becomes non-empty when media is found. Also fix stale log message that still said 'without reaction' after the filter was expanded to pass through attachment updates. * fix(bluebubbles): revert undici import, restore dispatcher-strip approach Revert the @claude bot's undici import in types.ts — it introduced a direct 'undici' dependency that is not declared in the BB extension's package.json and would break isolated plugin installs. Restore the original dispatcher-strip approach which is correct: the SSRF guard already completed validation upstream before calling this function as fetchImpl, so stripping the dispatcher does not weaken security. * fix(bluebubbles): remove dead empty-body recovery block in !rawBody guard The empty-body attachment-recovery block added in the earlier PR revision is now redundant because the main retry block was moved above the rawBody computation in 0d7d1c4208. Worse, that leftover block reassigned the (now-const) placeholder variable, throwing `TypeError: Assignment to constant variable` at runtime for image-only messages — breaking the very recovery path it was meant to protect (flagged by Codex on 4bfc2777). Remove the dead block; the up-front retry already handles the image-only case by recovering attachments before the rawBody computation, so once we reach the !rawBody guard with an empty body it is genuinely empty and should drop as before. * fix(ci): update raw-fetch allowlist line after dispatcher-strip revert 279dba17d2 reverted types.ts back to the dispatcher-strip approach, which put the `fetch(url, ...)` call at line 189 instead of line 198. Bump the allowlist entry to match so `lint:tmp:no-raw-channel-fetch` stops failing check-additional. * test(pdf-tool): update stale opus-4-6 constant to opus-4-7 `628b454eff feat: default Anthropic to Opus 4.7` bumped the bundled anthropic image default to `claude-opus-4-7` but missed updating the `ANTHROPIC_PDF_MODEL` constant in pdf-tool.model-config.test.ts. The tests now fail on any PR that runs the `checks-node-agentic-agents-plugins` shard because the resolver returns 4-7 while the test asserts 4-6. Bump the constant to 4-7 to match the bundled default. --------- Co-authored-by: Lobster <10343873+omarshahine@users.noreply.github.com> --- CHANGELOG.md | 1 + .../bluebubbles/src/attachments.test.ts | 89 ++++++++++++++++++- extensions/bluebubbles/src/attachments.ts | 54 ++++++++++- .../bluebubbles/src/inbound-dedupe.test.ts | 36 ++++++++ extensions/bluebubbles/src/inbound-dedupe.ts | 18 +++- .../bluebubbles/src/monitor-normalize.ts | 6 +- .../bluebubbles/src/monitor-processing.ts | 56 ++++++++++-- extensions/bluebubbles/src/monitor.ts | 16 +++- extensions/bluebubbles/src/types.ts | 10 ++- scripts/check-no-raw-channel-fetch.mjs | 2 +- 10 files changed, 270 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ab1ae9321c..7c9c525b87a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ Docs: https://docs.openclaw.ai - Cron/announce delivery: keep isolated announce `NO_REPLY` stripping case-insensitive across direct and text delivery, preserve structured media-only sends when a caption strips silent, and derive main-session awareness from the cleaned payloads so silent captions no longer leak stale `NO_REPLY` text. (#65016) Thanks @BKF-Gitty. - Sessions/Codex: skip redundant `delivery-mirror` transcript appends only when the latest assistant message has the same visible text, preventing duplicate visible replies on Codex-backed turns without suppressing repeated answers across turns. (#67185) Thanks @andyylin. - Auto-reply/prompt-cache: keep volatile inbound chat IDs out of the stable system prompt so task-scoped adapters can reuse prompt caches across runs, while preserving conversation metadata for the user turn and media-only messages. (#65071) Thanks @MonkeyLeeT. +- BlueBubbles/inbound: restore inbound image attachment downloads on Node 22+ by stripping incompatible bundled-undici dispatchers from the non-SSRF fetch path, accept `updated-message` webhooks carrying attachments, use event-type-aware dedup keys so attachment follow-ups are not rejected as duplicates, and retry attachment fetch from the BB API when the initial webhook arrives with an empty array. (#64105, #61861, #65430, #67510) Thanks @omarshahine. ## 2026.4.15-beta.1 diff --git a/extensions/bluebubbles/src/attachments.test.ts b/extensions/bluebubbles/src/attachments.test.ts index 735e832cc13..48330ee9286 100644 --- a/extensions/bluebubbles/src/attachments.test.ts +++ b/extensions/bluebubbles/src/attachments.test.ts @@ -1,6 +1,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import "./test-mocks.js"; -import { downloadBlueBubblesAttachment, sendBlueBubblesAttachment } from "./attachments.js"; +import { + downloadBlueBubblesAttachment, + fetchBlueBubblesMessageAttachments, + sendBlueBubblesAttachment, +} from "./attachments.js"; import { fetchBlueBubblesServerInfo, getCachedBlueBubblesPrivateApiStatus } from "./probe.js"; import type { PluginRuntime } from "./runtime-api.js"; import { setBlueBubblesRuntime } from "./runtime.js"; @@ -769,3 +773,86 @@ describe("sendBlueBubblesAttachment", () => { ).rejects.toThrow("chatGuid not found"); }); }); + +describe("fetchBlueBubblesMessageAttachments", () => { + beforeEach(() => { + mockFetch.mockReset(); + }); + + it("returns attachments from the BB API response", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: () => + Promise.resolve({ + data: { + attachments: [ + { + guid: "att-1", + mimeType: "image/jpeg", + transferName: "photo.jpg", + totalBytes: 1024, + }, + { + guid: "att-2", + mime_type: "image/png", + transfer_name: "screenshot.png", + total_bytes: 2048, + }, + ], + }, + }), + }); + const result = await fetchBlueBubblesMessageAttachments("msg-guid", { + baseUrl: "http://localhost:1234", + password: "test", + }); + expect(result).toHaveLength(2); + expect(result[0].guid).toBe("att-1"); + expect(result[0].mimeType).toBe("image/jpeg"); + expect(result[1].guid).toBe("att-2"); + expect(result[1].mimeType).toBe("image/png"); + }); + + it("returns empty array on non-ok HTTP response", async () => { + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 404, + }); + const result = await fetchBlueBubblesMessageAttachments("msg-guid", { + baseUrl: "http://localhost:1234", + password: "test", + }); + expect(result).toEqual([]); + }); + + it("returns empty array when data has no attachments", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ data: {} }), + }); + const result = await fetchBlueBubblesMessageAttachments("msg-guid", { + baseUrl: "http://localhost:1234", + password: "test", + }); + expect(result).toEqual([]); + }); + + it("includes entries without a guid (downstream download handles filtering)", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: () => + Promise.resolve({ + data: { + attachments: [{ mimeType: "image/jpeg" }, { guid: "att-valid", mimeType: "image/png" }], + }, + }), + }); + const result = await fetchBlueBubblesMessageAttachments("msg-guid", { + baseUrl: "http://localhost:1234", + password: "test", + }); + expect(result).toHaveLength(2); + expect(result[0].guid).toBeUndefined(); + expect(result[1].guid).toBe("att-valid"); + }); +}); diff --git a/extensions/bluebubbles/src/attachments.ts b/extensions/bluebubbles/src/attachments.ts index 83124b61b84..f5c3e4acddd 100644 --- a/extensions/bluebubbles/src/attachments.ts +++ b/extensions/bluebubbles/src/attachments.ts @@ -8,6 +8,7 @@ import { normalizeOptionalString, } from "openclaw/plugin-sdk/text-runtime"; import { resolveBlueBubblesServerAccount } from "./account-resolve.js"; +import { extractAttachments } from "./monitor-normalize.js"; import { assertMultipartActionOk, postMultipartFormData } from "./multipart.js"; import { fetchBlueBubblesServerInfo, @@ -26,8 +27,12 @@ import { type SsrFPolicy, } from "./types.js"; -function blueBubblesPolicy(allowPrivateNetwork: boolean | undefined): SsrFPolicy { - return allowPrivateNetwork ? { allowPrivateNetwork: true } : {}; +function blueBubblesPolicy(allowPrivateNetwork: boolean | undefined): SsrFPolicy | undefined { + // Pass `undefined` (not `{}`) for the non-private case so the non-SSRF fallback path + // is used. An empty `{}` policy routes through the SSRF guard, which blocks the + // localhost BB deployments that are the most common self-hosted setup. The opt-in + // private-network branch keeps the explicit policy. (#64105, #67510) + return allowPrivateNetwork ? { allowPrivateNetwork: true } : undefined; } export type BlueBubblesAttachmentOpts = { @@ -95,6 +100,51 @@ function readMediaFetchErrorCode(error: unknown): MediaFetchErrorCode | undefine : undefined; } +/** + * Fetch attachment metadata for a message from the BlueBubbles API. + * + * BlueBubbles sometimes fires the `new-message` webhook before attachment + * indexing is complete, so `attachments` arrives as `[]`. This function + * GETs the message by GUID and returns whatever attachments the server + * has indexed by now. (#65430, #67437) + */ +export async function fetchBlueBubblesMessageAttachments( + messageGuid: string, + opts: { + baseUrl: string; + password: string; + timeoutMs?: number; + allowPrivateNetwork?: boolean; + }, +): Promise { + const url = buildBlueBubblesApiUrl({ + baseUrl: opts.baseUrl, + path: `/api/v1/message/${encodeURIComponent(messageGuid)}`, + password: opts.password, + }); + // Pass undefined (not {}) when private network is not opted-in so the + // non-SSRF fallback path is used — an empty {} triggers the SSRF-guarded + // path which blocks localhost BB servers by default. (#64105) + const policy: SsrFPolicy | undefined = opts.allowPrivateNetwork + ? { allowPrivateNetwork: true } + : undefined; + const response = await blueBubblesFetchWithTimeout( + url, + { method: "GET" }, + opts.timeoutMs, + policy, + ); + if (!response.ok) { + return []; + } + const json = (await response.json()) as Record; + const data = json.data as Record | undefined; + if (!data) { + return []; + } + return extractAttachments(data); +} + export async function downloadBlueBubblesAttachment( attachment: BlueBubblesAttachment, opts: BlueBubblesAttachmentOpts & { maxBytes?: number } = {}, diff --git a/extensions/bluebubbles/src/inbound-dedupe.test.ts b/extensions/bluebubbles/src/inbound-dedupe.test.ts index cd78be834bc..46034ef8f20 100644 --- a/extensions/bluebubbles/src/inbound-dedupe.test.ts +++ b/extensions/bluebubbles/src/inbound-dedupe.test.ts @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it } from "vitest"; import { _resetBlueBubblesInboundDedupForTest, claimBlueBubblesInboundMessage, + resolveBlueBubblesInboundDedupeKey, } from "./inbound-dedupe.js"; async function claimAndFinalize(guid: string | undefined, accountId: string): Promise { @@ -56,3 +57,38 @@ describe("claimBlueBubblesInboundMessage", () => { expect(await claimAndFinalize("g1", "acc")).toBe("claimed"); }); }); + +describe("resolveBlueBubblesInboundDedupeKey", () => { + it("returns messageId for new-message events", () => { + expect(resolveBlueBubblesInboundDedupeKey({ messageId: "msg-1" })).toBe("msg-1"); + }); + + it("returns associatedMessageGuid for balloon events", () => { + expect( + resolveBlueBubblesInboundDedupeKey({ + messageId: "balloon-1", + balloonBundleId: "com.apple.messages.URLBalloonProvider", + associatedMessageGuid: "msg-1", + }), + ).toBe("msg-1"); + }); + + it("suffixes key with :updated for updated-message events", () => { + expect( + resolveBlueBubblesInboundDedupeKey({ messageId: "msg-1", eventType: "updated-message" }), + ).toBe("msg-1:updated"); + }); + + it("updated-message and new-message for same GUID produce distinct keys", () => { + const newKey = resolveBlueBubblesInboundDedupeKey({ messageId: "msg-1" }); + const updatedKey = resolveBlueBubblesInboundDedupeKey({ + messageId: "msg-1", + eventType: "updated-message", + }); + expect(newKey).not.toBe(updatedKey); + }); + + it("returns undefined when messageId is missing", () => { + expect(resolveBlueBubblesInboundDedupeKey({})).toBeUndefined(); + }); +}); diff --git a/extensions/bluebubbles/src/inbound-dedupe.ts b/extensions/bluebubbles/src/inbound-dedupe.ts index deffa49685f..1be0dadedb9 100644 --- a/extensions/bluebubbles/src/inbound-dedupe.ts +++ b/extensions/bluebubbles/src/inbound-dedupe.ts @@ -136,15 +136,27 @@ function sanitizeGuid(guid: string | undefined | null): string | null { export function resolveBlueBubblesInboundDedupeKey( message: Pick< NormalizedWebhookMessage, - "messageId" | "balloonBundleId" | "associatedMessageGuid" + "messageId" | "balloonBundleId" | "associatedMessageGuid" | "eventType" >, ): string | undefined { const balloonBundleId = message.balloonBundleId?.trim(); const associatedMessageGuid = message.associatedMessageGuid?.trim(); + let base: string | undefined; if (balloonBundleId && associatedMessageGuid) { - return associatedMessageGuid; + base = associatedMessageGuid; + } else { + base = message.messageId?.trim() || undefined; } - return message.messageId?.trim() || undefined; + if (!base) { + return undefined; + } + // `updated-message` events get a distinct key so they are not rejected as + // duplicates of the already-committed `new-message` for the same GUID. + // This lets attachment-carrying follow-up webhooks through. (#65430, #52277) + if (message.eventType === "updated-message") { + return `${base}:updated`; + } + return base; } export type InboundDedupeClaim = diff --git a/extensions/bluebubbles/src/monitor-normalize.ts b/extensions/bluebubbles/src/monitor-normalize.ts index 463e1939b1b..5df7edb6cde 100644 --- a/extensions/bluebubbles/src/monitor-normalize.ts +++ b/extensions/bluebubbles/src/monitor-normalize.ts @@ -34,7 +34,7 @@ function readNumberLike(record: Record | null, key: string): nu return parseFiniteNumber(record[key]); } -function extractAttachments(message: Record): BlueBubblesAttachment[] { +export function extractAttachments(message: Record): BlueBubblesAttachment[] { const raw = message["attachments"]; if (!Array.isArray(raw)) { return []; @@ -477,6 +477,8 @@ export type NormalizedWebhookMessage = { replyToId?: string; replyToBody?: string; replyToSender?: string; + /** Webhook event type preserved for dedup key differentiation. */ + eventType?: string; }; export type NormalizedWebhookReaction = { @@ -687,6 +689,7 @@ function extractMessagePayload(payload: Record): Record, + options?: { eventType?: string }, ): NormalizedWebhookMessage | null { const message = extractMessagePayload(payload); if (!message) { @@ -774,6 +777,7 @@ export function normalizeWebhookMessage( replyToId: replyMetadata.replyToId, replyToBody: replyMetadata.replyToBody, replyToSender: replyMetadata.replyToSender, + eventType: options?.eventType, }; } diff --git a/extensions/bluebubbles/src/monitor-processing.ts b/extensions/bluebubbles/src/monitor-processing.ts index e2ef93c070f..0e9016d73de 100644 --- a/extensions/bluebubbles/src/monitor-processing.ts +++ b/extensions/bluebubbles/src/monitor-processing.ts @@ -9,7 +9,10 @@ import { normalizeOptionalLowercaseString, normalizeOptionalString, } from "openclaw/plugin-sdk/text-runtime"; -import { downloadBlueBubblesAttachment } from "./attachments.js"; +import { + downloadBlueBubblesAttachment, + fetchBlueBubblesMessageAttachments, +} from "./attachments.js"; import { markBlueBubblesChatRead, sendBlueBubblesTyping } from "./chat.js"; import { resolveBlueBubblesConversationRoute } from "./conversation-route.js"; import { fetchBlueBubblesHistory } from "./history.js"; @@ -692,8 +695,52 @@ async function processMessageAfterDedupe( const isGroup = typeof groupFlag === "boolean" ? groupFlag : message.isGroup; const text = message.text.trim(); - const attachments = message.attachments ?? []; - const placeholder = buildMessagePlaceholder(message); + let attachments = message.attachments ?? []; + const baseUrl = normalizeSecretInputString(account.config.serverUrl); + const password = normalizeSecretInputString(account.config.password); + + // BlueBubbles may fire the webhook before attachment indexing is complete, + // so the initial `attachments` array can be empty for messages that actually + // have media. When the message text is empty (image-only) or this is an + // `updated-message` event, wait briefly and re-fetch from the BB API as a + // fallback for cases where BB doesn't send a follow-up webhook. (#65430, #67437) + // This must run before the !rawBody guard below, otherwise image-only messages + // with empty attachments are dropped before the retry can fire. + const retryMessageId = message.messageId?.trim(); + const shouldRetryAttachments = + attachments.length === 0 && + retryMessageId && + baseUrl && + password && + (text.length === 0 || message.eventType === "updated-message"); + if (shouldRetryAttachments) { + try { + await new Promise((resolve) => setTimeout(resolve, 2_000)); + const fetched = await fetchBlueBubblesMessageAttachments(retryMessageId, { + baseUrl, + password, + timeoutMs: 10_000, + allowPrivateNetwork: isPrivateNetworkOptInEnabled(account.config), + }); + if (fetched.length > 0) { + logVerbose( + core, + runtime, + `attachment retry found ${fetched.length} attachment(s) for msgId=${message.messageId}`, + ); + attachments = fetched; + } + } catch (err) { + logVerbose( + core, + runtime, + `attachment retry failed for msgId=${message.messageId}: ${String(err)}`, + ); + } + } + + // Recompute placeholder from resolved attachments (may have been updated by retry). + const placeholder = buildMessagePlaceholder({ ...message, attachments }); // Check if text is a tapback pattern (e.g., 'Loved "hello"') and transform to emoji format // For tapbacks, we'll append [[reply_to:N]] at the end; for regular messages, prepend it const tapbackContext = resolveTapbackContext(message); @@ -1019,9 +1066,6 @@ async function processMessageAfterDedupe( return; } - const baseUrl = normalizeSecretInputString(account.config.serverUrl); - const password = normalizeSecretInputString(account.config.password); - if (isGroup && !message.participants?.length && baseUrl && password) { try { const fetchedParticipants = await fetchBlueBubblesParticipantsForInboundMessage({ diff --git a/extensions/bluebubbles/src/monitor.ts b/extensions/bluebubbles/src/monitor.ts index f34f725dddb..79d74bb3db4 100644 --- a/extensions/bluebubbles/src/monitor.ts +++ b/extensions/bluebubbles/src/monitor.ts @@ -249,11 +249,22 @@ export async function handleBlueBubblesWebhookRequest( return true; } const reaction = normalizeWebhookReaction(payload); + // Normalize the webhook message early so the attachment-update detection + // below sees attachments under any supported wrapper format (`payload.data`, + // `payload.message`, `payload.data.message`, JSON-string payloads), not just + // raw `payload.data.attachments`. (#65430, #67510) + const message = reaction ? null : normalizeWebhookMessage(payload, { eventType }); + // BlueBubbles fires `updated-message` when attachments are indexed after the + // initial `new-message` (which may arrive with attachments: []). Let those + // through so the agent can ingest the image. (#65430) + const isAttachmentUpdate = + eventType === "updated-message" && (message?.attachments?.length ?? 0) > 0; if ( (eventType === "updated-message" || eventType === "message-reaction" || eventType === "reaction") && - !reaction + !reaction && + !isAttachmentUpdate ) { res.statusCode = 200; res.end("ok"); @@ -261,12 +272,11 @@ export async function handleBlueBubblesWebhookRequest( logVerbose( firstTarget.core, firstTarget.runtime, - `webhook ignored ${eventType || "event"} without reaction`, + `webhook ignored ${eventType || "event"} (no reaction or attachment update)`, ); } return true; } - const message = reaction ? null : normalizeWebhookMessage(payload); if (!message && !reaction) { res.statusCode = 400; res.end("invalid payload"); diff --git a/extensions/bluebubbles/src/types.ts b/extensions/bluebubbles/src/types.ts index 90a4dbafc0a..307fe2b1809 100644 --- a/extensions/bluebubbles/src/types.ts +++ b/extensions/bluebubbles/src/types.ts @@ -175,10 +175,18 @@ export async function blueBubblesFetchWithTimeout( await release(); } } + // Strip `dispatcher` from init — the SSRF guard may have attached a bundled-undici + // dispatcher that is incompatible with Node 22+'s built-in undici backing globalThis.fetch(). + // Passing it through causes a silent TypeError (invalid onRequestStart method). + // The SSRF validation already completed upstream in fetchWithSsrFGuard before calling + // this function as fetchImpl, so stripping the dispatcher does not weaken security. (#64105) + const { dispatcher: _dispatcher, ...safeInit } = (init ?? {}) as RequestInit & { + dispatcher?: unknown; + }; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { - return await fetch(url, { ...init, signal: controller.signal }); + return await fetch(url, { ...safeInit, signal: controller.signal }); } finally { clearTimeout(timer); } diff --git a/scripts/check-no-raw-channel-fetch.mjs b/scripts/check-no-raw-channel-fetch.mjs index 0d019d8eda4..ff5724257e0 100644 --- a/scripts/check-no-raw-channel-fetch.mjs +++ b/scripts/check-no-raw-channel-fetch.mjs @@ -15,7 +15,7 @@ const sourceRoots = ["src/channels", "src/routing", "src/line", "extensions"]; // code should be rejected and migrated to fetchWithSsrFGuard/shared channel helpers. const allowedRawFetchCallsites = new Set([ bundledPluginCallsite("bluebubbles", "src/test-harness.ts", 130), - bundledPluginCallsite("bluebubbles", "src/types.ts", 181), + bundledPluginCallsite("bluebubbles", "src/types.ts", 189), bundledPluginCallsite("browser", "src/browser/cdp.helpers.ts", 268), bundledPluginCallsite("browser", "src/browser/client-fetch.ts", 192), bundledPluginCallsite("browser", "src/browser/test-fetch.ts", 24),