openclaw - ✅(Solved) Fix Slack file attachments silently fail to download since 2026.4.5 (fetch-guard regression) [2 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#61862Fetched 2026-04-08 02:53:22
View on GitHub
Comments
2
Participants
3
Timeline
4
Reactions
0
Author
Timeline (top)
commented ×2cross-referenced ×2

Slack file attachments sent in DMs are no longer downloaded since upgrading to 2026.4.5 (from 2026.4.2). The file metadata is received (filename appears in the message as [Slack file: ...]), but the actual file content is never saved to media/inbound/. No errors appear in any logs.

Error Message

  1. No error in gateway.log or the verbose log The Slack media download uses a custom fetchImpl (createSlackMediaFetch) that adds the Authorization: Bearer <token> header. When the SSRF guard creates a pinned DNS dispatcher, the new routing logic may bypass the custom fetchImpl, causing the download to fail (likely returns an HTML error page from Slack). The error is invisible because of this catch-all in resolveSlackMedia (actions-*.js): If the download returns an HTML error page (auth failure), it is silently discarded.

Root Cause

The fetch-guard module (fetch-guard-*.js) was significantly rewritten in 2026.4.5. The key behavioral change is in fetchWithSsrFGuard:

Old (2026.4.2):

const response = await fetcher(parsedUrl.toString(), init);

New (2026.4.5):

const supportsDispatcherInit = params.fetchImpl !== void 0 
  && !isAmbientGlobalFetch({fetchImpl: params.fetchImpl, globalFetch: globalThis.fetch}) 
  || isMockedFetch(defaultFetch);

const response = Boolean(dispatcher) && !supportsDispatcherInit 
  ? await fetchWithRuntimeDispatcher(parsedUrl.toString(), init) 
  : await defaultFetch(parsedUrl.toString(), init);

The new code introduces:

  • fetchWithRuntimeDispatcher — calls undici.fetch directly, bypassing any custom fetchImpl
  • createPolicyDispatcherWithoutPinnedDns — new dispatcher creation path
  • isMockedFetch / isAmbientGlobalFetch — new routing checks
  • rewriteRedirectInitForMethod — new redirect handling
  • Pre-populated visited set: new Set([params.url]) instead of new Set()

The Slack media download uses a custom fetchImpl (createSlackMediaFetch) that adds the Authorization: Bearer <token> header. When the SSRF guard creates a pinned DNS dispatcher, the new routing logic may bypass the custom fetchImpl, causing the download to fail (likely returns an HTML error page from Slack).

Fix Action

Workaround

Files can be retrieved manually via the Slack API:

curl -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
  "https://slack.com/api/conversations.replies?channel=CHANNEL&ts=THREAD_TS" \
  | jq ".messages[].files[].url_private_download"
# Then download with:
curl -L -H "Authorization: Bearer $SLACK_BOT_TOKEN" "<url_private_download>" -o file.ext

PR fix notes

PR #1: fix(slack): reliably ingest Slack file images instead of filename placeholders

Description (problem / solution / changelog)

Summary

Fixes Slack inbound file/image ingestion regressions end-to-end (including Errol runtime validation):

  • Use files.info hydration when Slack events include file id without usable download URLs.
  • Use the correct media-read token path (userToken fallback) in monitor context, so file reads are not restricted to bot-token-only paths.
  • Harden Slack media fetch init handling to avoid passing incompatible fetch guard hook options into Node fetch, which caused silent download failure + filename-only placeholders.
  • Ensure staged inbound media filenames are unique across turns to avoid cross-turn collisions.
  • Add regression tests for id-only payload hydration and sandbox media staging collisions.

Validation

  • pnpm vitest extensions/slack/src/monitor/media.test.ts --run
  • pnpm vitest extensions/slack/src/monitor/message-handler/prepare.test.ts --run
  • pnpm vitest extensions/slack/src/monitor/message-handler/prepare-thread-context.test.ts --run
  • pnpm vitest src/auto-reply/reply.triggers.trigger-handling.stages-inbound-media-into-sandbox-workspace.test.ts --run
  • Live Errol Docker verification in Slack thread: image content now resolved (not placeholder-only filename).

AI assistance

This PR was AI-assisted (Hedwig/OpenClaw + Codex) and then validated with targeted tests and live runtime verification.

Related Slack file issues (tagging all matches)

Refs #50129 Refs #51050 Refs #62088 Refs #51458 Refs #62623 Refs #62551 Refs #61862 Refs #41657 Refs #45574 Refs #61850 Refs #36507 Refs #13634 Refs #44544 Refs #38457 Refs #47600 Refs #56508 Refs #52962 Refs #18426 Refs #62218 Refs #33368 Refs #15087 Refs #18642 Refs #7536 Refs #24681 Refs #23349 Refs #29304 Refs #7110 Refs #13740 Refs #15190 Refs #3595 Refs #3519 Refs #14258 Refs #6008

Changed files

  • extensions/slack/src/monitor/context.ts (modified, +3/-0)
  • extensions/slack/src/monitor/media.test.ts (modified, +58/-1)
  • extensions/slack/src/monitor/media.ts (modified, +84/-18)
  • extensions/slack/src/monitor/message-handler/prepare-thread-context.test.ts (modified, +109/-0)
  • extensions/slack/src/monitor/message-handler/prepare-thread-context.ts (modified, +30/-11)
  • extensions/slack/src/monitor/message-handler/prepare.ts (modified, +1/-1)
  • extensions/slack/src/monitor/monitor.media.test.ts (modified, +41/-2)
  • extensions/slack/src/monitor/provider.ts (modified, +1/-0)
  • src/auto-reply/reply.triggers.trigger-handling.stages-inbound-media-into-sandbox-workspace.test.ts (modified, +42/-6)
  • src/auto-reply/reply/stage-sandbox-media.ts (modified, +11/-4)

PR #62792: Fix Slack file access in channels and DMs

Description (problem / solution / changelog)

  • Tooling note: AI-assisted development (Hedwig/OpenClaw + Codex), with human validation and final review by @armsteadj1.

Summary

Describe the problem and fix in 2–5 bullets:

If this PR fixes a plugin beta-release blocker, title it fix(<plugin-id>): beta blocker - <summary> and link the matching Beta blocker: <plugin-name> - <summary> issue labeled beta-blocker. Contributors cannot label PRs, so the title is the PR-side signal for maintainers and automation.

  • Problem: Slack inbound image/file messages sometimes reached the model as filename placeholders only ([Slack file: IMG_4935.jpg]) instead of actual media content.
  • Why it matters: Image understanding and downstream workflows fail, causing repeated user retries and broken Slack UX.
  • What changed: Fixes the reproduced Slack filename-placeholder regression in this media ingestion path by hardening Slack media hydration/fetch, using the resolved media-read token path, and preventing cross-turn staged filename collisions.
  • What did NOT change (scope boundary): No UI changes, no outbound Slack behavior changes, and no claim that every historical Slack-file issue is resolved.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #50129
  • Related #51050
  • Related #51458
  • Related #62088
  • This PR fixes a bug or regression

Root Cause (if applicable)

For bug fixes or regressions, explain why this happened, not just what changed. Otherwise write N/A. If the cause is unclear, write Unknown.

  • Root cause: Slack file events can arrive with partial metadata and restricted URL access semantics; combined with brittle media fetch init handling and token-read path selection, the ingestion path sometimes failed and fell back to filename placeholders.
  • Missing detection / guardrail: Tests did not lock in this specific Slack file metadata/token-read + staged-media collision path.
  • Contributing context (if known): Threaded Slack flows with repeated filenames and file payload variation increased failure probability.

Regression Test Plan (if applicable)

For bug fixes or regressions, name the smallest reliable test coverage that should catch this. Otherwise write N/A.

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file:
    • extensions/slack/src/monitor/media.test.ts
    • extensions/slack/src/monitor/message-handler/prepare.test.ts
    • extensions/slack/src/monitor/message-handler/prepare-thread-context.test.ts
    • src/auto-reply/reply.triggers.trigger-handling.stages-inbound-media-into-sandbox-workspace.test.ts
  • Scenario the test should lock in: Slack file events with partial metadata still produce real media payloads, and repeated inbound basenames across turns do not collide.
  • Why this is the smallest reliable guardrail: It exercises the failing boundary directly without requiring a full external Slack E2E harness.
  • Existing test that already covers this (if any): Existing Slack media/prepare coverage exists; this PR extends edge-path coverage.
  • If no new test is added, why not: N/A

User-visible / Behavior Changes

List user-visible changes (including defaults/config). If none, write None.

Slack image/file messages now resolve as actual media content more reliably in the affected ingestion path instead of filename-only placeholders.

Diagram (if applicable)

For UI changes or non-trivial logic flows, include a small ASCII diagram reviewers can scan quickly. Otherwise write N/A.

Before:
[Slack file event] -> [media fetch/hydration path fails] -> [placeholder only]

After:
[Slack file event] -> [robust hydration + read-token path + safe fetch init] -> [media staged] -> [model sees image]New permissions/capabilities? (No)Secrets/tokens handling changed? (Yes)New/changed network calls? (No)Command/tool execution surface changed? (No)Data access scope changed? (No)If any Yes, explain risk + mitigation:Risk: token selection for Slack media reads changed in this path.Mitigation: constrained to existing configured Slack token sources and media-read flow only; no new secret source or broader access added.OS: macOS host + Docker runtimeRuntime/container: Errol gateway in Docker (openclaw:local)Model/provider: openai-codex/gpt-5.3-codex (Errol runtime default)Integration/channel (if any): Slack (threaded channel flow)Relevant config (redacted): Slack bot/app tokens configured; Errol isolated config/workspace/port.Send Slack thread message with attached image (same repro case where response only saw filename placeholder).Observe pre-fix behavior (placeholder-only).Apply fix branch, rebuild/restart Errol, resend same image flow.Assistant can access and reason over actual image content.Before: placeholder-only behavior reproduced.After: image content was successfully resolved and identified.Attach at least one:Failing test/log before + passing afterTrace/log snippetsScreenshot/recordingPerf numbers (if relevant)What you personally verified (not just CI), and how:Verified scenarios:Reproduced filename-placeholder failure in live Slack thread.Verified post-fix behavior resolved actual image content in same thread flow.Edge cases checked:Slack file metadata hydration path.Repeated inbound basename staging across turns.What you did not verify:Broad claim across all historical Slack-file issues.Full multi-workspace/perf matrix beyond this runtime path.I replied to or resolved every bot review conversation I addressed in this PR.I left unresolved only the conversations that still need reviewer or maintainer judgment.If a bot review conversation is addressed by this PR, resolve that conversation yourself. Do not leave bot review conversation cleanup for maintainers.Backward compatible? (Yes)Config/env changes? (No)Migration needed? (No)If yes, exact upgrade steps:List only real risks for this PR. Add/remove entries as needed. If none, write None.Risk:Slack tenant/app-specific file payload variance may expose additional untested edge cases.Mitigation:Coverage added for the reproduced edge path and constrained changes to Slack media ingestion only.Risk:Token-read path change could behave differently in uncommon token setups.Mitigation:Uses existing resolved read-token path with fallback; no new token source introduced.

## Changed files

- `extensions/slack/src/monitor/context.ts` (modified, +3/-0)
- `extensions/slack/src/monitor/media.test.ts` (modified, +173/-0)
- `extensions/slack/src/monitor/media.ts` (modified, +204/-36)
- `extensions/slack/src/monitor/message-handler/prepare-content.ts` (modified, +3/-0)
- `extensions/slack/src/monitor/message-handler/prepare-thread-context.test.ts` (modified, +109/-0)
- `extensions/slack/src/monitor/message-handler/prepare-thread-context.ts` (modified, +37/-11)
- `extensions/slack/src/monitor/message-handler/prepare.ts` (modified, +2/-0)
- `extensions/slack/src/monitor/monitor.media.test.ts` (modified, +41/-2)
- `extensions/slack/src/monitor/provider.ts` (modified, +1/-0)
- `src/auto-reply/reply.triggers.trigger-handling.stages-inbound-media-into-sandbox-workspace.test.ts` (modified, +66/-6)
- `src/auto-reply/reply/stage-sandbox-media.ts` (modified, +39/-4)
- `src/commands/agent-via-gateway.test.ts` (modified, +25/-26)

Code Example

const response = await fetcher(parsedUrl.toString(), init);

---

const supportsDispatcherInit = params.fetchImpl !== void 0 
  && !isAmbientGlobalFetch({fetchImpl: params.fetchImpl, globalFetch: globalThis.fetch}) 
  || isMockedFetch(defaultFetch);

const response = Boolean(dispatcher) && !supportsDispatcherInit 
  ? await fetchWithRuntimeDispatcher(parsedUrl.toString(), init) 
  : await defaultFetch(parsedUrl.toString(), init);

---

try {
    const fetched = await fetchRemoteMedia({...});
    // ... process file ...
} catch {
    return null;  // ← ALL errors silently swallowed
}

---

if (fetched.contentType?.split(";")[0]?.trim().toLowerCase() === "text/html" 
    || looksLikeHtmlBuffer(fetched.buffer)) return null;

---

catch (err) {
       logVerbose?.(`slack media download failed for ${file.name}: ${err?.message ?? err}`);
       return null;
   }

---

curl -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
  "https://slack.com/api/conversations.replies?channel=CHANNEL&ts=THREAD_TS" \
  | jq ".messages[].files[].url_private_download"
# Then download with:
curl -L -H "Authorization: Bearer $SLACK_BOT_TOKEN" "<url_private_download>" -o file.ext
RAW_BUFFERClick to expand / collapse

Bug Report

Summary

Slack file attachments sent in DMs are no longer downloaded since upgrading to 2026.4.5 (from 2026.4.2). The file metadata is received (filename appears in the message as [Slack file: ...]), but the actual file content is never saved to media/inbound/. No errors appear in any logs.

Environment

  • OpenClaw version: 2026.4.5 (worked on 2026.4.2)
  • Node.js: v25.9.0
  • OS: macOS (arm64, Darwin 25.3.0)
  • Slack mode: Socket Mode
  • File type tested: Markdown (.md), 17KB

Reproduction

  1. Upgrade from 2026.4.2 to 2026.4.5
  2. Send a file attachment in a Slack DM to the bot
  3. The bot receives the message text and the filename placeholder ([Slack file: example.md])
  4. No file is saved to ~/.openclaw/media/inbound/
  5. No error in gateway.log or the verbose log

Verification

  • The Slack API returns the file correctly with valid url_private and url_private_download URLs
  • Direct curl download with the same bot token succeeds (HTTP 200, correct content)
  • The conversations.replies API confirms file objects with all expected fields
  • The bot token has files:read scope and auth.test succeeds

Root Cause Analysis

The fetch-guard module (fetch-guard-*.js) was significantly rewritten in 2026.4.5. The key behavioral change is in fetchWithSsrFGuard:

Old (2026.4.2):

const response = await fetcher(parsedUrl.toString(), init);

New (2026.4.5):

const supportsDispatcherInit = params.fetchImpl !== void 0 
  && !isAmbientGlobalFetch({fetchImpl: params.fetchImpl, globalFetch: globalThis.fetch}) 
  || isMockedFetch(defaultFetch);

const response = Boolean(dispatcher) && !supportsDispatcherInit 
  ? await fetchWithRuntimeDispatcher(parsedUrl.toString(), init) 
  : await defaultFetch(parsedUrl.toString(), init);

The new code introduces:

  • fetchWithRuntimeDispatcher — calls undici.fetch directly, bypassing any custom fetchImpl
  • createPolicyDispatcherWithoutPinnedDns — new dispatcher creation path
  • isMockedFetch / isAmbientGlobalFetch — new routing checks
  • rewriteRedirectInitForMethod — new redirect handling
  • Pre-populated visited set: new Set([params.url]) instead of new Set()

The Slack media download uses a custom fetchImpl (createSlackMediaFetch) that adds the Authorization: Bearer <token> header. When the SSRF guard creates a pinned DNS dispatcher, the new routing logic may bypass the custom fetchImpl, causing the download to fail (likely returns an HTML error page from Slack).

Silent Failure Chain

The error is invisible because of this catch-all in resolveSlackMedia (actions-*.js):

try {
    const fetched = await fetchRemoteMedia({...});
    // ... process file ...
} catch {
    return null;  // ← ALL errors silently swallowed
}

Combined with the HTML safety check:

if (fetched.contentType?.split(";")[0]?.trim().toLowerCase() === "text/html" 
    || looksLikeHtmlBuffer(fetched.buffer)) return null;

If the download returns an HTML error page (auth failure), it is silently discarded.

Suggested Fixes

  1. Immediate: Log errors in the catch block of resolveSlackMedia instead of silently returning null:

    catch (err) {
        logVerbose?.(`slack media download failed for ${file.name}: ${err?.message ?? err}`);
        return null;
    }
  2. Root fix: Ensure the custom fetchImpl is always used as the primary fetcher in fetchWithSsrFGuard, regardless of dispatcher state. The SSRF pinned-DNS dispatcher should wrap/augment the custom fetch, not replace it.

  3. Defensive: When looksLikeHtmlBuffer triggers on a non-HTML file, log the content-type mismatch as a warning.

Additional Changes in fetch-guard (2026.4.5)

For completeness, other changes in the same file that may interact:

  • loadUndiciRuntimeDeps() now validates and exports undici.fetch (previously only Agent, EnvHttpProxyAgent, ProxyAgent)
  • New dropBodyHeaders and rewriteRedirectInitForMethod for redirect handling
  • New createPolicyDispatcherWithoutPinnedDns path when params.pinDns === false
  • visited set initialized with the original URL (was empty before)

Workaround

Files can be retrieved manually via the Slack API:

curl -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
  "https://slack.com/api/conversations.replies?channel=CHANNEL&ts=THREAD_TS" \
  | jq ".messages[].files[].url_private_download"
# Then download with:
curl -L -H "Authorization: Bearer $SLACK_BOT_TOKEN" "<url_private_download>" -o file.ext

extent analysis

TL;DR

The most likely fix for the issue with Slack file attachments not being downloaded since upgrading to OpenClaw version 2026.4.5 is to modify the fetchWithSsrFGuard function to ensure the custom fetchImpl is always used as the primary fetcher.

Guidance

  • Log errors in the catch block of resolveSlackMedia instead of silently returning null to identify potential issues.
  • Modify the fetchWithSsrFGuard function to prioritize the custom fetchImpl over the dispatcher, ensuring that the custom fetch is used for Slack media downloads.
  • Consider adding a defensive check to log content-type mismatches when looksLikeHtmlBuffer triggers on a non-HTML file.

Example

To log errors in the catch block of resolveSlackMedia, you can modify the code as follows:

catch (err) {
    logVerbose?.(`slack media download failed for ${file.name}: ${err?.message ?? err}`);
    return null;
}

This will help identify any errors that may be occurring during the file download process.

Notes

The issue is specific to OpenClaw version 2026.4.5 and may not affect other versions. The suggested fixes are based on the provided information and may require additional modifications to fully resolve the issue.

Recommendation

Apply the suggested fixes, starting with logging errors in the catch block of resolveSlackMedia and modifying the fetchWithSsrFGuard function to prioritize the custom fetchImpl. This should help resolve the issue with Slack file attachments not being downloaded.

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

openclaw - ✅(Solved) Fix Slack file attachments silently fail to download since 2026.4.5 (fetch-guard regression) [2 pull requests, 2 comments, 3 participants]