Skip to content

fix(slack): strip dispatcher option before passing to globalThis.fetch#63401

Closed
arbaleast wants to merge 1 commit intoopenclaw:mainfrom
arbaleast:fix/slack-dispatcher
Closed

fix(slack): strip dispatcher option before passing to globalThis.fetch#63401
arbaleast wants to merge 1 commit intoopenclaw:mainfrom
arbaleast:fix/slack-dispatcher

Conversation

@arbaleast
Copy link
Copy Markdown

Bug

Slack download-file silently fails on Node.js v25 because globalThis.fetch does not support the dispatcher option. The error is swallowed by the catch block.

Root Cause

In createSlackMediaFetch(), when dispatcher is in init and !isMockedFetch(globalThis.fetch) is true, it uses fetchWithRuntimeDispatcher (undici). But when the condition is false (e.g. mocked fetch), it falls back to globalThis.fetch with the dispatcher option still present — causing TypeError: fetch failed in Node.js v25.

Fix

Strip dispatcher (and redirect) from init options before passing to globalThis.fetch. Undici-backed path is unaffected since it uses fetchWithRuntimeDispatcher directly with the original options.

Fixes #63393

In Node.js v25, globalThis.fetch does not support the dispatcher option
(only undici.fetch does). Strip dispatcher from init options while routing
dispatcher-backed requests through fetchWithRuntimeDispatcher (undici.fetch).
Fixes silent download-file failures on Node.js v25.
@openclaw-barnacle openclaw-barnacle Bot added channel: slack Channel integration: slack size: XS labels Apr 8, 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: 7016fcf0fd

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +80 to +82
const { dispatcher: _dispatcher, redirect: _redirect, ...rest } = init ?? {};
const fetchImpl =
"dispatcher" in (init ?? {}) && !isMockedFetch(globalThis.fetch)
_dispatcher !== undefined && !isMockedFetch(globalThis.fetch)
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 Keep dispatcher in runtime fetch request options

Extracting _dispatcher into rest removes it from all subsequent calls, including the fetchWithRuntimeDispatcher branch. That branch is used for dispatcher-backed guarded fetches, so Slack media requests no longer carry the pinned/proxy dispatcher and can fail in environments that depend on it, while also dropping the transport guarantees guarded fetch intended to enforce. The dispatcher field should only be stripped for the globalThis.fetch fallback path.

Useful? React with 👍 / 👎.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 8, 2026

Greptile Summary

This PR fixes a TypeError in createSlackMediaFetch on Node.js v25 by stripping dispatcher (and redirect) from the init options before passing them to globalThis.fetch. The globalThis.fetch fallback path is correctly fixed, but the undici path (fetchWithRuntimeDispatcher) also receives { ...rest, redirect: "manual" } without the original dispatcher, silently dropping the pinned SSRF dispatcher that fetchWithSsrFGuard constructed from SLACK_MEDIA_SSRF_POLICY. This voids the DNS-pinning guarantee on the undici path and needs to be addressed before merge.

Confidence Score: 4/5

Not safe to merge as-is: the undici path drops the pinned SSRF dispatcher, voiding DNS-rebinding protection.

The fix is directionally correct and solves the reported Node v25 TypeError for the globalThis.fetch fallback path. However, _dispatcher is also stripped from the undici (fetchWithRuntimeDispatcher) path, silently bypassing the SSRF protection that fetchWithSsrFGuard set up via SLACK_MEDIA_SSRF_POLICY. This is a present security-boundary defect introduced by the PR, warranting a 4 rather than 5.

extensions/slack/src/monitor/media.ts — specifically createSlackMediaFetch lines 80–85

Vulnerabilities

  • SSRF protection bypass in extensions/slack/src/monitor/media.ts createSlackMediaFetch: the pinned dispatcher created by fetchWithSsrFGuard/createPinnedDispatcher (which enforces DNS-pinning per SLACK_MEDIA_SSRF_POLICY) is stripped from the options but not forwarded to fetchWithRuntimeDispatcher. The actual TCP connection re-resolves DNS via undici's global dispatcher, opening a DNS-rebinding window on the undici path.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/slack/src/monitor/media.ts
Line: 80-85

Comment:
**Pinned SSRF dispatcher is silently dropped on the undici path**

`_dispatcher` is destructured out of `rest` but never forwarded to `fetchWithRuntimeDispatcher`. When `fetchWithSsrFGuard` calls this fetch impl, it already resolved DNS and built a pinned dispatcher (`createPinnedDispatcher`) specifically to prevent DNS-rebinding/SSRF. Stripping it here causes undici to fall back to its global dispatcher, re-resolving DNS at connection time and voiding the SSRF guarantee. The `globalThis.fetch` path is correctly fixed — the undici path needs the same value forwarded:

```suggestion
    const { dispatcher: _dispatcher, redirect: _redirect, ...rest } = init ?? {};
    const usesUndici = _dispatcher !== undefined && !isMockedFetch(globalThis.fetch);
    const fetchImpl = usesUndici ? fetchWithRuntimeDispatcher : globalThis.fetch;
    return fetchImpl(parsed.href, {
      ...rest,
      ...(usesUndici && _dispatcher !== undefined ? { dispatcher: _dispatcher } : {}),
      redirect: "manual",
    });
```

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

Reviews (1): Last reviewed commit: "fix(slack): strip dispatcher option befo..." | Re-trigger Greptile

Comment on lines +80 to +85
const { dispatcher: _dispatcher, redirect: _redirect, ...rest } = init ?? {};
const fetchImpl =
"dispatcher" in (init ?? {}) && !isMockedFetch(globalThis.fetch)
_dispatcher !== undefined && !isMockedFetch(globalThis.fetch)
? fetchWithRuntimeDispatcher
: globalThis.fetch;
return fetchImpl(parsed.href, { ...init, redirect: "manual" });
return fetchImpl(parsed.href, { ...rest, redirect: "manual" });
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 security Pinned SSRF dispatcher is silently dropped on the undici path

_dispatcher is destructured out of rest but never forwarded to fetchWithRuntimeDispatcher. When fetchWithSsrFGuard calls this fetch impl, it already resolved DNS and built a pinned dispatcher (createPinnedDispatcher) specifically to prevent DNS-rebinding/SSRF. Stripping it here causes undici to fall back to its global dispatcher, re-resolving DNS at connection time and voiding the SSRF guarantee. The globalThis.fetch path is correctly fixed — the undici path needs the same value forwarded:

Suggested change
const { dispatcher: _dispatcher, redirect: _redirect, ...rest } = init ?? {};
const fetchImpl =
"dispatcher" in (init ?? {}) && !isMockedFetch(globalThis.fetch)
_dispatcher !== undefined && !isMockedFetch(globalThis.fetch)
? fetchWithRuntimeDispatcher
: globalThis.fetch;
return fetchImpl(parsed.href, { ...init, redirect: "manual" });
return fetchImpl(parsed.href, { ...rest, redirect: "manual" });
const { dispatcher: _dispatcher, redirect: _redirect, ...rest } = init ?? {};
const usesUndici = _dispatcher !== undefined && !isMockedFetch(globalThis.fetch);
const fetchImpl = usesUndici ? fetchWithRuntimeDispatcher : globalThis.fetch;
return fetchImpl(parsed.href, {
...rest,
...(usesUndici && _dispatcher !== undefined ? { dispatcher: _dispatcher } : {}),
redirect: "manual",
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/slack/src/monitor/media.ts
Line: 80-85

Comment:
**Pinned SSRF dispatcher is silently dropped on the undici path**

`_dispatcher` is destructured out of `rest` but never forwarded to `fetchWithRuntimeDispatcher`. When `fetchWithSsrFGuard` calls this fetch impl, it already resolved DNS and built a pinned dispatcher (`createPinnedDispatcher`) specifically to prevent DNS-rebinding/SSRF. Stripping it here causes undici to fall back to its global dispatcher, re-resolving DNS at connection time and voiding the SSRF guarantee. The `globalThis.fetch` path is correctly fixed — the undici path needs the same value forwarded:

```suggestion
    const { dispatcher: _dispatcher, redirect: _redirect, ...rest } = init ?? {};
    const usesUndici = _dispatcher !== undefined && !isMockedFetch(globalThis.fetch);
    const fetchImpl = usesUndici ? fetchWithRuntimeDispatcher : globalThis.fetch;
    return fetchImpl(parsed.href, {
      ...rest,
      ...(usesUndici && _dispatcher !== undefined ? { dispatcher: _dispatcher } : {}),
      redirect: "manual",
    });
```

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

@mjamiv
Copy link
Copy Markdown
Contributor

mjamiv commented Apr 8, 2026

+1 — this is a direct hit for us. We run 4 OpenClaw 2026.4.5 agents in NVIDIA OpenShell sandboxes behind an HTTP CONNECT proxy (10.200.0.1:3128). Slack file downloads (images, PDFs, audio) flow through createSlackMediaFetchglobalThis.fetch.

We've been carrying a local workaround in our openclaw-fetch-guard-patch.py that sets dispatcher = null to prevent exactly this class of failure — Node 22's built-in globalThis.fetch rejects npm undici v8 dispatchers with invalid onRequestStart method, and the error is silently swallowed by the bare catch block in the Slack download path.

This PR is the proper upstream fix. Happy to provide production confirmation across all 4 agents once this lands in a release.

Environment: 4 sandboxes, OpenClaw 2026.4.5, Node 22, Slack Socket Mode, all traffic via CONNECT proxy.

@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 27, 2026

Closing this as duplicate or superseded after Codex automated review.

This PR is superseded by the newer Slack media dispatcher work in #64022/#63905. Current main still has the underlying dispatcher behavior, but this PR's one-file diff has unresolved review concerns and overlaps a later PR that carries the follow-up fix/test discussion and production confirmation.

Best possible solution:

Close this PR as superseded and continue the Slack media dispatcher fix in #64022/#63905, or a maintainer-owned replacement, with tests that explicitly settle the runtime-fetch versus pinned-dispatcher contract.

What I checked:

So I’m closing this here and keeping the remaining discussion on the canonical linked item.

Codex Review notes: model gpt-5.5, reasoning high; reviewed against 2dba9e6a765a.

@clawsweeper clawsweeper Bot closed this Apr 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: slack Channel integration: slack size: XS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Slack download-file fails silently on Node.js v25 — globalThis.fetch does not support dispatcher option

2 participants