fix(slack): strip dispatcher option before passing to globalThis.fetch#63401
fix(slack): strip dispatcher option before passing to globalThis.fetch#63401arbaleast wants to merge 1 commit intoopenclaw:mainfrom
Conversation
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.
There was a problem hiding this comment.
💡 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".
| const { dispatcher: _dispatcher, redirect: _redirect, ...rest } = init ?? {}; | ||
| const fetchImpl = | ||
| "dispatcher" in (init ?? {}) && !isMockedFetch(globalThis.fetch) | ||
| _dispatcher !== undefined && !isMockedFetch(globalThis.fetch) |
There was a problem hiding this comment.
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 SummaryThis PR fixes a Confidence Score: 4/5Not 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
|
| 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" }); |
There was a problem hiding this 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:
| 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.|
+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 ( We've been carrying a local workaround in our 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. |
|
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. |
Bug
Slack
download-filesilently fails on Node.js v25 becauseglobalThis.fetchdoes not support thedispatcheroption. The error is swallowed by the catch block.Root Cause
In
createSlackMediaFetch(), whendispatcheris ininitand!isMockedFetch(globalThis.fetch)is true, it usesfetchWithRuntimeDispatcher(undici). But when the condition is false (e.g. mocked fetch), it falls back toglobalThis.fetchwith thedispatcheroption still present — causingTypeError: fetch failedin Node.js v25.Fix
Strip
dispatcher(andredirect) from init options before passing toglobalThis.fetch. Undici-backed path is unaffected since it usesfetchWithRuntimeDispatcherdirectly with the original options.Fixes #63393