fix(msteams): SharePoint media fetch fails on Node 24+ (#63396)#64652
fix(msteams): SharePoint media fetch fails on Node 24+ (#63396)#64652BradGroux merged 2 commits intoopenclaw:mainfrom
Conversation
Greptile SummaryThis PR fixes a silent failure of SharePoint/OneDrive file downloads on Node 24+ by introducing a The security boundary is sound: Confidence Score: 5/5Safe to merge — the fix is correct and well-tested, with SSRF protection maintained and error visibility substantially improved. All findings are P2. The No files require special attention beyond the minor body-not-consumed note in Prompt To Fix All With AIThis is a comment left during a code review.
Path: extensions/msteams/src/attachments/remote-media.ts
Line: 33-36
Comment:
**Response body not consumed on HTTP error**
When `response.ok` is false the function throws without draining or cancelling the body. Undici (Node's built-in fetch) keeps the socket checked-out until the body is GC-ed or the connection times out, and emits a "body was not consumed" diagnostic in some configurations. Because this path can be hit on every SharePoint 401/403/5xx, repeated failures would stall the keep-alive pool sooner than expected.
```suggestion
if (!response.ok) {
await response.body?.cancel().catch(() => {});
const statusText = response.statusText ? ` ${response.statusText}` : "";
throw new Error(`HTTP ${response.status}${statusText}`);
}
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "fix(msteams): fix SharePoint media fetch..." | Re-trigger Greptile |
| if (!response.ok) { | ||
| const statusText = response.statusText ? ` ${response.statusText}` : ""; | ||
| throw new Error(`HTTP ${response.status}${statusText}`); | ||
| } |
There was a problem hiding this comment.
Response body not consumed on HTTP error
When response.ok is false the function throws without draining or cancelling the body. Undici (Node's built-in fetch) keeps the socket checked-out until the body is GC-ed or the connection times out, and emits a "body was not consumed" diagnostic in some configurations. Because this path can be hit on every SharePoint 401/403/5xx, repeated failures would stall the keep-alive pool sooner than expected.
| if (!response.ok) { | |
| const statusText = response.statusText ? ` ${response.statusText}` : ""; | |
| throw new Error(`HTTP ${response.status}${statusText}`); | |
| } | |
| if (!response.ok) { | |
| await response.body?.cancel().catch(() => {}); | |
| const statusText = response.statusText ? ` ${response.statusText}` : ""; | |
| throw new Error(`HTTP ${response.status}${statusText}`); | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/attachments/remote-media.ts
Line: 33-36
Comment:
**Response body not consumed on HTTP error**
When `response.ok` is false the function throws without draining or cancelling the body. Undici (Node's built-in fetch) keeps the socket checked-out until the body is GC-ed or the connection times out, and emits a "body was not consumed" diagnostic in some configurations. Because this path can be hit on every SharePoint 401/403/5xx, repeated failures would stall the keep-alive pool sooner than expected.
```suggestion
if (!response.ok) {
await response.body?.cancel().catch(() => {});
const statusText = response.statusText ? ` ${response.statusText}` : "";
throw new Error(`HTTP ${response.status}${statusText}`);
}
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0c2b04c039
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (params.useDirectFetch && params.fetchImpl) { | ||
| fetched = await fetchRemoteMediaDirect({ | ||
| url: params.url, | ||
| fetchImpl: params.fetchImpl, | ||
| maxBytes: params.maxBytes, |
There was a problem hiding this comment.
Preserve SSRF IP checks in direct fetch mode
When useDirectFetch is enabled, this branch bypasses channel.media.fetchRemoteMedia(...ssrfPolicy) and therefore skips the strict fetchWithSsrFGuard path that enforces pinned-DNS/private-IP blocking. The current callers provide fetchImpl wrappers based on safeFetchWithPolicy without a resolveFn, so they only enforce hostname suffix allowlists; an allowlisted domain with attacker-controlled DNS (or rebinding) can now resolve to internal/private addresses that were blocked before this commit.
Useful? React with 👍 / 👎.
0c2b04c to
15b468e
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 15b468edb1
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // allowlist via `safeFetchWithPolicy`, so skip `fetchRemoteMedia`'s | ||
| // strict SSRF dispatcher (incompatible with Node 24+ / undici v7; | ||
| // see issue #63396). | ||
| useDirectFetch: true, |
There was a problem hiding this comment.
Keep strict SSRF IP validation on attachment downloads
This now forces all attachment URLs through useDirectFetch, which bypasses channel.media.fetchRemoteMedia(...ssrfPolicy) and its pinned-DNS/private-IP guard; the replacement path (fetchWithAuthFallback -> safeFetchWithPolicy) is called without resolveFn, so it only checks hostname suffixes. That means allowlisted domains can now resolve/redirect to private/internal IPs that were previously blocked, which is a security regression for generic attachment downloads (not just the SharePoint Node 24 workaround).
Useful? React with 👍 / 👎.
| params.logger?.warn?.("msteams attachment download failed", { | ||
| error: err instanceof Error ? err.message : String(err), | ||
| host: safeHostForLog(candidate.url), |
There was a problem hiding this comment.
Redact URL-bearing fetch errors before warning logs
This logs raw err.message, but direct-fetch failures can bubble up messages from safeFetch that include full redirect URLs (for example blocked redirect targets). In Teams/SharePoint flows those URLs can carry signed query parameters, so failed downloads can leak sensitive link tokens into warn logs. Please redact URL content (or log host/status only) before attaching error metadata.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6c64df6136
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| response = await safeFetchWithPolicy({ | ||
| url, | ||
| policy: params.policy, | ||
| fetchFn: params.fetchFn, |
There was a problem hiding this comment.
Reinstate IP-level SSRF checks for Bot Framework downloads
This change replaces fetchWithSsrFGuard(...ssrfPolicy) with safeFetchWithPolicy(...) for Bot Framework attachment fetches, but no resolveFn is supplied, so only hostname suffix allowlisting is enforced. In this path, an allowlisted hostname (or redirect target) that resolves to a private/loopback address will no longer be blocked, which is a security regression from the previous pinned-DNS/private-IP guard behavior used before this commit; the same pattern is applied to both attachment info and view fetches.
Useful? React with 👍 / 👎.
6c64df6 to
3f956c6
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3f956c68d5
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (Number.isFinite(length) && length > params.maxBytes) { | ||
| throw new Error(`content length ${length} exceeds maxBytes ${params.maxBytes}`); |
There was a problem hiding this comment.
Cancel oversized response bodies before throwing
When Content-Length is above maxBytes, this branch throws immediately without canceling the response stream. In the new direct-fetch path, that leaves the body unread, so repeated oversized attachments can keep undici sockets busy and reduce connection reuse (a regression from the previous fetchRemoteMedia(...).release() cleanup path). Please cancel the body before throwing on this early-exit path.
Useful? React with 👍 / 👎.
| params.logger?.warn?.("msteams SharePoint reference download failed", { | ||
| error: err instanceof Error ? err.message : String(err), | ||
| name, |
There was a problem hiding this comment.
Redact SharePoint fetch errors before warning logs
This warning logs raw err.message, but failures from safeFetchWithPolicy can include full redirect URLs. In SharePoint flows those URLs often carry signed query parameters, so a blocked/failed download can leak tokenized links into logs. Please redact URL content (or log host/status only) before attaching the error text.
Useful? React with 👍 / 👎.
3f956c6 to
1b15f1a
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1b15f1a44d
ℹ️ 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".
| if (!response.ok) { | ||
| params.logger?.warn?.("msteams botFramework attachmentView non-ok", { | ||
| status: response.status, | ||
| }); | ||
| return undefined; |
There was a problem hiding this comment.
Cancel Bot Framework response body before early return
After replacing fetchWithSsrFGuard(...).release() with safeFetchWithPolicy, this branch returns on non-OK responses without draining or canceling response.body. In undici, unread bodies can keep connections occupied until GC, so repeated failed attachment downloads (for example 401/404/500) can reduce connection reuse and hurt throughput. The previous implementation always released resources in a finally block, so this is a regression in the new path.
Useful? React with 👍 / 👎.
| params.logger?.warn?.("msteams botFramework attachmentInfo fetch failed", { | ||
| error: err instanceof Error ? err.message : String(err), | ||
| }); |
There was a problem hiding this comment.
Redact Bot Framework fetch errors before warning logs
This logs raw err.message from safeFetchWithPolicy, whose thrown messages can include full redirect target URLs. Bot Framework attachment redirects can carry signed query parameters, so failed/blocked fetches can leak sensitive tokens into warning logs. Please redact URL/query content (or log host/status only) before attaching the error string.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ccd0fded6a
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // allowlist via `safeFetchWithPolicy`, so skip `fetchRemoteMedia`'s | ||
| // strict SSRF dispatcher (incompatible with Node 24+ / undici v7; | ||
| // see issue #63396). | ||
| useDirectFetch: true, |
There was a problem hiding this comment.
Reinstate pinned DNS for direct attachment downloads
Setting useDirectFetch: true here forces all attachment fetches onto fetchRemoteMediaDirect, which calls fetchImpl without fetchWithSsrFGuard's pinned dispatcher. Fresh evidence versus earlier SSRF findings: even though resolveFn is now threaded through, safeFetchWithPolicy only performs a preflight DNS/IP check and then issues a separate fetch, so DNS can change between validation and connect. That is a security regression from the previous pinned-DNS path for environments that rely on strict SSRF guarantees (especially when allowHosts includes tenant-configured domains).
Useful? React with 👍 / 👎.
ccd0fde to
ee7bc12
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ee7bc12811
ℹ️ 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".
| if (!response.ok) { | ||
| const statusText = response.statusText ? ` ${response.statusText}` : ""; | ||
| throw new Error(`HTTP ${response.status}${statusText}`); |
There was a problem hiding this comment.
Cancel direct-fetch response before throwing on HTTP failure
When useDirectFetch is enabled, this branch throws on non-2xx status without draining or canceling response.body. In Node/undici, unread bodies can keep connections occupied until GC, so repeated failed attachment downloads (for example 401/403/500 responses) can reduce connection reuse and hurt throughput. The previous fetchRemoteMedia(...).release() path cleaned up transport resources even on failure.
Useful? React with 👍 / 👎.
| const contentLength = response.headers.get("content-length"); | ||
| if (contentLength && Number(contentLength) > params.maxBytes) { | ||
| return undefined; |
There was a problem hiding this comment.
Cancel Bot Framework view body on size precheck rejection
This early return exits when Content-Length exceeds maxBytes but leaves the response body unread. In the new safeFetchWithPolicy path there is no release() cleanup, so repeated oversized Bot Framework attachments can keep undici sockets busy and degrade download throughput. The prior guarded-fetch implementation always released resources in a finally block.
Useful? React with 👍 / 👎.
… (openclaw#64652) * fix(msteams): fix SharePoint media fetch on Node 24+ and stop swallowing errors (openclaw#63396) * fix(msteams): extend Node 24 dispatcher fix to Bot Framework attachment view downloads
… (openclaw#64652) * fix(msteams): fix SharePoint media fetch on Node 24+ and stop swallowing errors (openclaw#63396) * fix(msteams): extend Node 24 dispatcher fix to Bot Framework attachment view downloads
… (openclaw#64652) * fix(msteams): fix SharePoint media fetch on Node 24+ and stop swallowing errors (openclaw#63396) * fix(msteams): extend Node 24 dispatcher fix to Bot Framework attachment view downloads
… (openclaw#64652) * fix(msteams): fix SharePoint media fetch on Node 24+ and stop swallowing errors (openclaw#63396) * fix(msteams): extend Node 24 dispatcher fix to Bot Framework attachment view downloads
… (openclaw#64652) * fix(msteams): fix SharePoint media fetch on Node 24+ and stop swallowing errors (openclaw#63396) * fix(msteams): extend Node 24 dispatcher fix to Bot Framework attachment view downloads
Summary
Fixes silent failure of SharePoint/OneDrive file downloads on Node 24+ (undici v7).
Root cause
Two bugs compounded:
fetchRemoteMediaroutes throughcreatePinnedDispatcherwhich is incompatible with undici v7's stricter dispatcher interface shipped in Node 24+. When a caller passes a customfetchImplthat eventually delegates toglobalThis.fetch, the pinned dispatcher built from bundled undici@7.24.7 gets handed to Node's built-in undici v7, which rejects it withTypeError: fetch failed | invalid onRequestStart method.catch {}blocks indownload.ts,graph.ts, andbot-framework.ts, leaving no logs or diagnostic signal.Fix
useDirectFetchpath indownloadAndStoreMSTeamsRemoteMediathat bypassesfetchRemoteMedia's strict SSRF dispatcher when the caller'sfetchImplhas already validated the URL against a hostname allowlist (viasafeFetchWithPolicy). This is only opted into by the two MSTeams call sites that pre-validate (download attachments + SharePoint reference downloads), so other callers still get the strict dispatcher.maxBytesviareadResponseWithLimit(including a Content-Length pre-check) so rogue responses cannot drive RSS past the configured limit.catch {}blocks indownload.ts,graph.ts, andbot-framework.tswith structuredlogger.warncalls so operators can actually see fetch failures. The logger is threaded fromresolveMSTeamsInboundMedia(which already has access toMSTeamsMonitorLogger) through the attachments path.Test plan
remote-media.test.ts: verifies the direct-fetch path bypassesfetchRemoteMedia, surfaces HTTP errors, enforces Content-Length caps, and still falls back to the strict dispatcher whenuseDirectFetchis not set.attachments.test.ts: pin thelogger.warncontract on failure, and verify success paths stay silent.pnpm test extensions/msteams(773 tests across 61 files, all green).pnpm tsgo,pnpm lint,pnpm format,pnpm buildall green.Fixes #63396
Generated with Claude Code