Skip to content

fix(msteams): fix inline pasted image downloads#50044

Closed
jlian wants to merge 1 commit intoopenclaw:mainfrom
jlian:fix/msteams-inline-image-download
Closed

fix(msteams): fix inline pasted image downloads#50044
jlian wants to merge 1 commit intoopenclaw:mainfrom
jlian:fix/msteams-inline-image-download

Conversation

@jlian
Copy link
Copy Markdown

@jlian jlian commented Mar 18, 2026

Summary

Fixes inline pasted image downloads in MSTeams conversations. The Teams API returns hostedContents attachments without contentBytes, so images are never saved.

Changes

  • Download images via contentUrl using the bot's bearer token when contentBytes is missing
  • Handle both hosted content URLs and direct download URLs
  • Properly extract content type from response headers

Testing

  • Verified on v2026.3.13-1 in DM conversations
  • Without fix: images detected but not saved to disk (0 bytes / missing files)
  • With fix: images correctly downloaded and available for processing

Fixes #50043
Supersedes #10902 (closed by stale bot)
Related to #5448 (locked)

@openclaw-barnacle openclaw-barnacle bot added channel: msteams Channel integration: msteams size: S labels Mar 18, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 18, 2026

Greptile Summary

This PR fixes inline pasted image downloads in MS Teams conversations by adding a $value endpoint fallback in downloadGraphHostedContent when the Graph collection API returns contentBytes: null, appending /views/original to Bot Framework attachment URLs, and handling mixed messages (regular files + inline images) via a new code path in resolveMSTeamsInboundMedia. The fix also expands DEFAULT_MEDIA_AUTH_HOST_ALLOWLIST to cover three Bot Framework regional variants for proper auth-token forwarding.

Key changes:

  • graph.ts: When contentBytes is absent/null, the new code fetches the binary from ${messageUrl}/hostedContents/{id}/$value using fetchWithSsrFGuard with the bearer token, a streaming bounded-read loop to enforce maxBytes, and a content-length pre-check. await release() is correctly called and headerMime is sourced from the response content-type header. One minor issue: when the content-length pre-check exceeds maxBytes, the response body is never explicitly cancelled, which can stall connection pool return in undici.
  • download.ts: rewriteBotFrameworkUrl reliably appends /views/original to /v3/attachments/{id} paths and is applied to both attachment contentUrl values and inline <img src> URLs. The regex correctly prevents double-rewrites.
  • inbound-media.ts: The mixed-message branch (mediaList.length > 0) acquires a token directly and calls downloadGraphHostedContent, correctly appending results to the existing mediaList. Token acquisition is wrapped in try/catch. The branch does not forward fetchFn or ssrfPolicy to downloadGraphHostedContent, meaning the $value fetch in that code path uses the global fetch and no SSRF allowlist.
  • shared.ts: Three explicit smba.* hostnames added to DEFAULT_MEDIA_AUTH_HOST_ALLOWLIST (AMER default, EMEA, APAC). The broader "trafficmanager.net" suffix in DEFAULT_MEDIA_HOST_ALLOWLIST is unchanged, preserving non-auth coverage for other potential regional variants.

Confidence Score: 4/5

  • Safe to merge with one minor connection-pool consideration in the oversized-content skip path.
  • The core fix (fetching from $value when contentBytes is null) is correctly implemented with SSRF guard, bounded streaming, proper auth headers, and awaited release(). Previous review concerns (wrong param names, missing await on release, SSRF bypass, broken dedup logic, wrong property access) have all been addressed. Tests correctly exercise the new $value path using contentBytes: null. The one remaining finding — not cancelling the response body when the content-length pre-check fires — is a resource management suggestion rather than a correctness issue, as it only affects HTTP connection pool efficiency under load.
  • Pay close attention to extensions/msteams/src/attachments/graph.ts around the content-length pre-check (line 224) where the response body should be explicitly cancelled to return the connection to the undici pool promptly.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/msteams/src/attachments/graph.ts
Line: 224-225

Comment:
**Response body not cancelled when content-length exceeds limit**

When `contentLength > params.maxBytes`, the branch is taken but the response body is never consumed or explicitly cancelled. In Node.js (undici-backed `fetch`), an unconsumed response body prevents the underlying connection from being returned to the pool. Under load — e.g., many messages containing oversized images — this can exhaust the HTTP connection pool.

`release()` unpins the dispatcher guard but does not drain or cancel the body stream.

The fix is to cancel the body before leaving the branch:

```suggestion
            if (contentLength > params.maxBytes) {
              // Skip oversized content without buffering; cancel the body to
              // free the underlying connection back to the pool.
              await valRes.body?.cancel();
```

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

Last reviewed commit: "fix(msteams): fix in..."

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: ff45fe5d56

ℹ️ 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".

@jlian jlian force-pushed the fix/msteams-inline-image-download branch from ff45fe5 to 23f0831 Compare March 18, 2026 22:17
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: 23f0831c2e

ℹ️ 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".

@jlian jlian force-pushed the fix/msteams-inline-image-download branch from 23f0831 to 8a1f348 Compare March 18, 2026 22:27
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: 8a1f348dd7

ℹ️ 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".

@jlian jlian force-pushed the fix/msteams-inline-image-download branch from 8a1f348 to 40a8ad1 Compare March 18, 2026 22:41
@jlian
Copy link
Copy Markdown
Author

jlian commented Mar 18, 2026

All review comments addressed in the latest push. @greptileai @codex please re-review.

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: 40a8ad122a

ℹ️ 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".

@jlian jlian force-pushed the fix/msteams-inline-image-download branch from 40a8ad1 to a56b641 Compare March 18, 2026 23:10
@jlian
Copy link
Copy Markdown
Author

jlian commented Mar 18, 2026

All comments from round 2 addressed — P0 (wrong param names), P1s (release leak, duplicate media), P2 (test coverage). @greptileai @codex please re-review.

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: a56b641114

ℹ️ 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".

@jlian jlian force-pushed the fix/msteams-inline-image-download branch from a56b641 to 56f635c Compare March 18, 2026 23:20
@jlian
Copy link
Copy Markdown
Author

jlian commented Mar 18, 2026

Round 3 addressed: reverted broken dedup (P0), switched to streaming bounded read for maxBytes (P2), kept simple mediaList.length === 0 guard. @greptileai please re-review.

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: 56f635ca8c

ℹ️ 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".

@jlian jlian force-pushed the fix/msteams-inline-image-download branch from 56f635c to 3ace8b0 Compare March 18, 2026 23:38
@jlian
Copy link
Copy Markdown
Author

jlian commented Mar 18, 2026

Round 4: properly handles mixed file+inline messages by calling downloadGraphHostedContent directly (hostedContents only, no attachment duplication) when media already exists from direct downloads. @greptileai please re-review.

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: 3ace8b0463

ℹ️ 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".

@jlian jlian force-pushed the fix/msteams-inline-image-download branch from 3ace8b0 to d6d1cb6 Compare March 18, 2026 23:47
@jlian
Copy link
Copy Markdown
Author

jlian commented Mar 18, 2026

Round 5: fixed tokenProvider crash (P0 — was calling as function, now uses .getAccessToken()), fixed test to use contentBytes: null so $value path is actually exercised. @greptileai please re-review.

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: d6d1cb6d62

ℹ️ 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".

@jlian jlian force-pushed the fix/msteams-inline-image-download branch from d6d1cb6 to 9a97d69 Compare March 18, 2026 23:55
@jlian
Copy link
Copy Markdown
Author

jlian commented Mar 18, 2026

Round 6: wrapped mixed-message hostedContent path in try/catch (getAccessToken failure handling), added ssrfPolicy param. All 3 new P1s addressed. @greptileai please re-review.

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: 9a97d69855

ℹ️ 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".

@jlian jlian force-pushed the fix/msteams-inline-image-download branch from 9a97d69 to 497da20 Compare March 19, 2026 00:20
@jlian
Copy link
Copy Markdown
Author

jlian commented Mar 19, 2026

Round 7: gated Graph fallback on actual <img> tags in HTML (not just any rich-text), added reader.cancel() for oversized streams, removed explicit undefined params. @greptileai please re-review.

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

return {
media: [...sharePointMedia, ...hosted.media, ...attachmentMedia],

P2 Badge Deduplicate hosted-content results before appending attachment media

Fresh evidence in this commit is that downloadGraphHostedContent() now fills hosted.media from $value even when contentBytes is null, but this return still concatenates hosted.media and attachmentMedia blindly. When the Graph /attachments payload also includes the message's text/html attachment, downloadMSTeamsAttachments() will re-download the same pasted image from its <img src>, so the new hosted-content recovery path can surface duplicate media entries for a single inline image.

ℹ️ 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".

@jlian jlian force-pushed the fix/msteams-inline-image-download branch from 497da20 to b3be68a Compare March 19, 2026 00:49
@jlian
Copy link
Copy Markdown
Author

jlian commented Mar 19, 2026

Round 8: restored trafficmanager.net in download allowlist for regional Bot Framework endpoints (smba.eu, smba.ap), kept explicit regional variants in auth allowlist. Reverted hasInlineImages gate back to hasHtmlAttachments to preserve Graph fallback for non-img HTML attachments. @greptileai please re-review.

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: b3be68a65b

ℹ️ 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 +59 to +60
const hasHtmlAttachments = attachments.some((att) =>
String(att.contentType ?? "").startsWith("text/html"),
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 Gate hostedContents fallback on actual inline-image evidence

In channel replies, extractMSTeamsConversationMessageId() keeps the thread-root ;messageid= suffix from conversation.id, and buildMSTeamsGraphMessageUrls() then probes both the reply URL and the root message URL. Because this new gate keys off any text/html attachment instead of actual inline-image markup, a reply that only contains rich-text HTML plus a normal file can now append the thread root's hosted image to the current message. Please gate this path on htmlSummary.imgTags (or equivalent inline-image evidence) rather than contentType === text/html alone.

Useful? React with 👍 / 👎.

Comment on lines +93 to 95
if (hosted.media.length > 0) {
mediaList = [...mediaList, ...hosted.media];
break;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Deduplicate hostedContents against inline images already saved

If the HTML <img src> is already downloadable, downloadMSTeamsAttachments() has saved it earlier via extractInlineImageCandidates() in extensions/msteams/src/attachments/download.ts:208-227. This branch then appends hosted.media without any dedupe, so a pasted image that resolves both through the HTML URL and through Graph hostedContents will be surfaced twice. That gives downstream code duplicate media entries for a single image.

Useful? React with 👍 / 👎.

Comment on lines +87 to 91
const hosted = await downloadGraphHostedContent({
messageUrl,
tokenProvider,
accessToken: token,
maxBytes,
allowHosts,
authAllowHosts: params.authAllowHosts,
preserveFilenames,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reuse the configured fetch policy in the mixed hostedContents path

This mixed-message fast path calls downloadGraphHostedContent() directly, but unlike downloadMSTeamsGraphMedia() in extensions/msteams/src/attachments/graph.ts:403-410 it never threads through the ssrfPolicy derived from allowHosts. Deployments that intentionally narrow the Teams media allowlist will therefore still fetch messageUrl/hostedContents from Graph whenever an HTML attachment accompanies already-downloaded media, silently bypassing the configured host policy for that class of messages.

Useful? React with 👍 / 👎.

Comment on lines +224 to +225
if (contentLength > params.maxBytes) {
// Skip oversized content without buffering.
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.

P2 Response body not cancelled when content-length exceeds limit

When contentLength > params.maxBytes, the branch is taken but the response body is never consumed or explicitly cancelled. In Node.js (undici-backed fetch), an unconsumed response body prevents the underlying connection from being returned to the pool. Under load — e.g., many messages containing oversized images — this can exhaust the HTTP connection pool.

release() unpins the dispatcher guard but does not drain or cancel the body stream.

The fix is to cancel the body before leaving the branch:

Suggested change
if (contentLength > params.maxBytes) {
// Skip oversized content without buffering.
if (contentLength > params.maxBytes) {
// Skip oversized content without buffering; cancel the body to
// free the underlying connection back to the pool.
await valRes.body?.cancel();
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/attachments/graph.ts
Line: 224-225

Comment:
**Response body not cancelled when content-length exceeds limit**

When `contentLength > params.maxBytes`, the branch is taken but the response body is never consumed or explicitly cancelled. In Node.js (undici-backed `fetch`), an unconsumed response body prevents the underlying connection from being returned to the pool. Under load — e.g., many messages containing oversized images — this can exhaust the HTTP connection pool.

`release()` unpins the dispatcher guard but does not drain or cancel the body stream.

The fix is to cancel the body before leaving the branch:

```suggestion
            if (contentLength > params.maxBytes) {
              // Skip oversized content without buffering; cancel the body to
              // free the underlying connection back to the pool.
              await valRes.body?.cancel();
```

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

@jlian jlian closed this Mar 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: msteams Channel integration: msteams size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MSTeams: inline pasted images not downloaded (hostedContents contentBytes missing)

1 participant