Skip to content

MSTeams: Image attachments not downloaded in bot DM chats (3 bugs) #24797

@themapisnottheterritory

Description

Summary

Images sent in personal (1:1) bot DM chats are never downloaded. Three bugs in the msteams extension's media download pipeline cause all image downloads to fail silently.

Environment

  • OpenClaw 2026.2.22
  • Microsoft 365 Business
  • Bot Framework SDK (via @microsoft/agents-hosting)

Bug 1: Wrong conversation ID format for Graph API

File: extensions/msteams/src/attachments/graph.tsbuildMSTeamsGraphMessageUrls

Bot Framework uses a:xxx conversation IDs for personal chats, but the Graph API requires 19:{userId}_{botAppId}@unq.gbl.spaces. The Graph request returns "Invalid ThreadId".

Fix: Translate the Bot Framework conversation ID before passing to resolveMSTeamsInboundMedia:

if (isDirectMessage && conversationId.startsWith("a:") && from.aadObjectId && appId) {
  graphConversationId = `19:${from.aadObjectId}_${appId}@unq.gbl.spaces`;
}

Bug 2: Graph fallback blocked by non-HTML attachments

File: extensions/msteams/src/monitor-handler/inbound-media.ts

The Graph fallback only triggers when ALL attachments are text/html (attachments.every(...)). Image messages include non-HTML metadata attachments alongside the HTML content, which blocks the fallback path entirely.

Fix: Change every() to some() — trigger Graph fallback when ANY HTML attachment is present.

Bug 3: Hosted content bytes not returned inline

File: extensions/msteams/src/attachments/graph.tsdownloadGraphHostedContent

The /hostedContents list endpoint returns metadata but contentBytes is null for bot DM chats. The code skips items without inline bytes (if (!contentBytes) { continue; }). The actual binary image data IS available at the individual $value endpoint: /hostedContents/{id}/$value.

Fix: When contentBytes is empty, fetch the binary content from the $value endpoint:

if (!buffer && item.id) {
  const valueUrl = `${messageUrl}/hostedContents/${encodeURIComponent(item.id)}/$value`;
  const valueRes = await fetchFn(valueUrl, {
    headers: { Authorization: `Bearer ${accessToken}` },
  });
  if (valueRes.ok) {
    buffer = Buffer.from(await valueRes.arrayBuffer());
  }
}

Diagnosis method

  • Gateway logs showed "graph media fetch empty" for every image message
  • Manual Graph API calls confirmed the conversation ID format mismatch (404 with a: ID, success with 19: ID)
  • Manual fetch of /hostedContents/{id}/$value returned the full image (32KB JPEG)
  • All three patches together resolve the issue completely

Labels

bug, msteams

Metadata

Metadata

Assignees

No one assigned

    Labels

    staleMarked as stale due to inactivity

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions