Truncate oversized Teams webhook payloads to prevent HTTP 413 errors#2160
Conversation
When an adaptive card payload exceeds the Teams webhook size limit (~28KB), progressively remove body items from the card and append a truncation notice. This prevents the entire alert flow from failing when a single message is too large for the Teams endpoint. Co-Authored-By: Michael Myaskovsky <michael@elementary-data.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
👋 @devin-ai-integration[bot] |
…k markdown for italic Co-Authored-By: Michael Myaskovsky <michael@elementary-data.com>
📝 WalkthroughWalkthroughThe change adds a 27 KiB payload size limit to the Teams webhook integration, builds and measures the full webhook payload before sending, and implements iterative truncation of adaptive card body elements with a truncation notice and a minimal-card fallback when needed. Changes
Sequence Diagram(s)sequenceDiagram
participant Caller
participant WebhookModule as TeamsWebhook
participant TeamsAPI as "Microsoft Teams\nWebhook API"
Caller->>WebhookModule: send_adaptive_card(card)
WebhookModule->>WebhookModule: _build_payload(card) -> payload
WebhookModule->>WebhookModule: json.dumps(payload) -> size
alt size <= TEAMS_PAYLOAD_SIZE_LIMIT
WebhookModule->>TeamsAPI: POST payload
else size > TEAMS_PAYLOAD_SIZE_LIMIT
WebhookModule->>WebhookModule: _truncate_card(card) (loop remove last body)
WebhookModule->>WebhookModule: if still too large -> _minimal_card()
WebhookModule->>TeamsAPI: POST truncated/minimal payload
end
TeamsAPI-->>WebhookModule: 200/response
WebhookModule-->>Caller: result
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
📝 Coding Plan
Comment |
Co-Authored-By: Michael Myaskovsky <michael@elementary-data.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
elementary/messages/messaging_integrations/teams_webhook.py (1)
84-93: Consider using list unpacking instead of concatenation.Static analysis flags
body + [_truncation_notice_item()]on lines 85 and 90. Using unpacking is more idiomatic and slightly more efficient.♻️ Proposed refactor
while len(body) > 1: - payload = _build_payload({**card, "body": body + [_truncation_notice_item()]}) + payload = _build_payload({**card, "body": [*body, _truncation_notice_item()]}) if len(json.dumps(payload)) <= TEAMS_PAYLOAD_SIZE_LIMIT: break body.pop() - truncated = {**card, "body": body + [_truncation_notice_item()]} + truncated = {**card, "body": [*body, _truncation_notice_item()]} if len(json.dumps(_build_payload(truncated))) > TEAMS_PAYLOAD_SIZE_LIMIT: return _minimal_card(card) return truncated🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@elementary/messages/messaging_integrations/teams_webhook.py` around lines 84 - 93, The code repeatedly concatenates lists via body + [_truncation_notice_item()] which static analysis flags; modify the two usages inside the loop and when constructing truncated to use list unpacking instead (e.g., [*body, _truncation_notice_item()]) so you still pass a new list into _build_payload and json.dumps while avoiding the concatenation idiom; ensure changes reference the same symbols (_build_payload, _truncation_notice_item, body, truncated, TEAMS_PAYLOAD_SIZE_LIMIT, _minimal_card) and preserve the existing logic and size checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@elementary/messages/messaging_integrations/teams_webhook.py`:
- Around line 84-93: The code repeatedly concatenates lists via body +
[_truncation_notice_item()] which static analysis flags; modify the two usages
inside the loop and when constructing truncated to use list unpacking instead
(e.g., [*body, _truncation_notice_item()]) so you still pass a new list into
_build_payload and json.dumps while avoiding the concatenation idiom; ensure
changes reference the same symbols (_build_payload, _truncation_notice_item,
body, truncated, TEAMS_PAYLOAD_SIZE_LIMIT, _minimal_card) and preserve the
existing logic and size checks.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a29959c0-4d77-41eb-afe8-82a2cefb6711
📒 Files selected for processing (1)
elementary/messages/messaging_integrations/teams_webhook.py
| return { | ||
| "type": "TextBlock", | ||
| "text": "_... Content truncated due to message size limits. " | ||
| "View full details in Elementary Cloud._", |
There was a problem hiding this comment.
this pr is in the package, not all package users have elementary cloud
There was a problem hiding this comment.
Good catch — removed the Elementary Cloud references from both truncation messages.
Co-Authored-By: Michael Myaskovsky <michael@elementary-data.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@elementary/messages/messaging_integrations/teams_webhook.py`:
- Around line 63-91: The current _minimal_card preserves all top-level fields
from card so large non-body fields (e.g., actions) can still make the payload
exceed TEAMS_PAYLOAD_SIZE_LIMIT; change _minimal_card to construct a strict
minimal payload (only keys required by _build_payload / Teams adaptive card:
type, body with the truncation TextBlock, and any mandatory schema/version keys)
instead of spreading **card, and update _truncate_card (and the similar fallback
at lines 94–106) to validate the final payload size by calling _build_payload
and, if still too large, return this strict minimal payload; reference
_minimal_card, _truncate_card, _build_payload, _truncation_notice_item and
TEAMS_PAYLOAD_SIZE_LIMIT when making the change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9ed56b2f-2b2c-40d4-8a58-30684a965ef2
📒 Files selected for processing (1)
elementary/messages/messaging_integrations/teams_webhook.py
| def _minimal_card(card: dict) -> dict: | ||
| return { | ||
| **card, | ||
| "body": [ | ||
| { | ||
| "type": "TextBlock", | ||
| "text": "Alert content too large to display in Teams.", | ||
| "wrap": True, | ||
| "weight": "bolder", | ||
| } | ||
| ], | ||
| } | ||
|
|
||
|
|
||
| def _truncate_card(card: dict) -> dict: | ||
| body: List[Dict[str, Any]] = list(card.get("body", [])) | ||
| if not body: | ||
| return card | ||
|
|
||
| while len(body) > 1: | ||
| payload = _build_payload({**card, "body": body + [_truncation_notice_item()]}) | ||
| if len(json.dumps(payload)) <= TEAMS_PAYLOAD_SIZE_LIMIT: | ||
| break | ||
| body.pop() | ||
|
|
||
| truncated = {**card, "body": body + [_truncation_notice_item()]} | ||
| if len(json.dumps(_build_payload(truncated))) > TEAMS_PAYLOAD_SIZE_LIMIT: | ||
| return _minimal_card(card) | ||
| return truncated |
There was a problem hiding this comment.
Minimal fallback can still exceed limit if non-body fields are large.
_minimal_card() preserves **card, so large top-level fields (for example actions) can keep the payload oversized. In that case, send_adaptive_card() can still post a >27KiB payload and hit 413 again.
Suggested hard-limit fallback
def _minimal_card(card: dict) -> dict:
- return {
- **card,
- "body": [
- {
- "type": "TextBlock",
- "text": "Alert content too large to display in Teams.",
- "wrap": True,
- "weight": "bolder",
- }
- ],
- }
+ # Keep only required/adaptive-card identity fields to guarantee small payload.
+ minimal = {
+ "type": card.get("type", "AdaptiveCard"),
+ "version": card.get("version", "1.4"),
+ "body": [
+ {
+ "type": "TextBlock",
+ "text": "Alert content too large to display in Teams.",
+ "wrap": True,
+ "weight": "bolder",
+ }
+ ],
+ }
+ if "$schema" in card:
+ minimal["$schema"] = card["$schema"]
+ return minimal def send_adaptive_card(webhook_url: str, card: dict) -> requests.Response:
@@
if len(payload_json) > TEAMS_PAYLOAD_SIZE_LIMIT:
@@
card = _truncate_card(card)
payload = _build_payload(card)
+ if len(json.dumps(payload)) > TEAMS_PAYLOAD_SIZE_LIMIT:
+ raise MessagingIntegrationError(
+ "Teams webhook payload still exceeds size limit after truncation."
+ )Also applies to: 94-106
🧰 Tools
🪛 Ruff (0.15.6)
[warning] 83-83: Consider [*body, _truncation_notice_item()] instead of concatenation
Replace with [*body, _truncation_notice_item()]
(RUF005)
[warning] 88-88: Consider [*body, _truncation_notice_item()] instead of concatenation
Replace with [*body, _truncation_notice_item()]
(RUF005)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@elementary/messages/messaging_integrations/teams_webhook.py` around lines 63
- 91, The current _minimal_card preserves all top-level fields from card so
large non-body fields (e.g., actions) can still make the payload exceed
TEAMS_PAYLOAD_SIZE_LIMIT; change _minimal_card to construct a strict minimal
payload (only keys required by _build_payload / Teams adaptive card: type, body
with the truncation TextBlock, and any mandatory schema/version keys) instead of
spreading **card, and update _truncate_card (and the similar fallback at lines
94–106) to validate the final payload size by calling _build_payload and, if
still too large, return this strict minimal payload; reference _minimal_card,
_truncate_card, _build_payload, _truncation_notice_item and
TEAMS_PAYLOAD_SIZE_LIMIT when making the change.
Summary
Fixes a production issue where the
send-alertsAirflow task for Prendio was failing because the alert adaptive card payload exceeded the Microsoft Teams webhook size limit, resulting in an HTTP 413 error that bubbled up asCouldNotSendAlertsErrorand failed the entire alert flow.The fix proactively measures the JSON payload size before sending. If it exceeds 27KB (just under Teams' ~28KB limit for Adaptive Cards), it progressively removes body items from the end of the card and appends a truncation notice, then sends the reduced payload. If even a single body item is too large, it falls back to a minimal card.
Key changes in
teams_webhook.py:_build_payload()helper fromsend_adaptive_card()for reuse_truncate_card()which progressively drops trailing body items until the payload fits_minimal_card()fallback when truncation alone isn't enough (single oversized body item)_truncation_notice_item()for the user-facing truncation messagesend_adaptive_card()Review & Testing Checklist for Human
_minimal_carduses**cardspread — non-body fields (e.g.actionsif ever added) are preserved, which could theoretically keep the payload oversized. In practiceformat_adaptive_card()only produces small top-level fields (type,version,msteams), so this is low risk but worth noting._truncate_cardcovering: payload under limit (no truncation), payload over limit (truncation occurs), single-body-item fallback to minimal card, empty body.Notes
send-alerts-76bf501d-...). The Teams destination5f726761-...was returningHTTP error 413inside a 200 response, which the existinglen(response.text) > 1check correctly caught but couldn't recover from.Link to Devin session: https://app.devin.ai/sessions/d746de1f2743489995cc5070957c91c6