Skip to content

Commit f7b1130

Browse files
devin-ai-integration[bot]Michael Myaskovsky
andcommitted
Truncate oversized Teams webhook payloads to prevent HTTP 413 errors
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>
1 parent 31e7528 commit f7b1130

1 file changed

Lines changed: 52 additions & 3 deletions

File tree

elementary/messages/messaging_integrations/teams_webhook.py

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import json
12
from datetime import datetime, timezone
23
from http import HTTPStatus
3-
from typing import Any, Optional
4+
from typing import Any, Dict, List, Optional
45

56
import requests
67
from ratelimit import limits, sleep_and_retry
@@ -25,6 +26,10 @@
2526

2627
Channel: TypeAlias = Optional[str]
2728
ONE_SECOND = 1
29+
# Teams webhook payload size limit in bytes. The actual limit is 28KB for
30+
# Adaptive Cards, but we use a slightly lower threshold to leave room for
31+
# the envelope (message wrapper, attachment metadata, etc.).
32+
TEAMS_PAYLOAD_SIZE_LIMIT = 27 * 1024
2833

2934

3035
class TeamsWebhookHttpError(MessagingIntegrationError):
@@ -36,8 +41,8 @@ def __init__(self, response: requests.Response):
3641
)
3742

3843

39-
def send_adaptive_card(webhook_url: str, card: dict) -> requests.Response:
40-
payload = {
44+
def _build_payload(card: dict) -> dict:
45+
return {
4146
"type": "message",
4247
"attachments": [
4348
{
@@ -48,6 +53,50 @@ def send_adaptive_card(webhook_url: str, card: dict) -> requests.Response:
4853
],
4954
}
5055

56+
57+
def _truncation_notice_item() -> Dict[str, Any]:
58+
return {
59+
"type": "TextBlock",
60+
"text": "... Content truncated due to message size limits. "
61+
"View full details in Elementary Cloud.",
62+
"wrap": True,
63+
"isSubtle": True,
64+
"italic": True,
65+
}
66+
67+
68+
def _truncate_card(card: dict) -> dict:
69+
"""Progressively remove body items from the card until the payload fits
70+
within the Teams size limit. A truncation notice is appended so the
71+
recipient knows content was removed."""
72+
body: List[Dict[str, Any]] = list(card.get("body", []))
73+
if not body:
74+
return card
75+
76+
while len(body) > 1:
77+
payload = _build_payload({**card, "body": body + [_truncation_notice_item()]})
78+
if len(json.dumps(payload)) <= TEAMS_PAYLOAD_SIZE_LIMIT:
79+
break
80+
body.pop() # remove the last body item
81+
82+
return {**card, "body": body + [_truncation_notice_item()]}
83+
84+
85+
def send_adaptive_card(webhook_url: str, card: dict) -> requests.Response:
86+
payload = _build_payload(card)
87+
88+
# Proactively truncate if the payload exceeds the Teams size limit.
89+
payload_json = json.dumps(payload)
90+
if len(payload_json) > TEAMS_PAYLOAD_SIZE_LIMIT:
91+
logger.warning(
92+
"Teams webhook payload size (%d bytes) exceeds limit (%d bytes), "
93+
"truncating card body",
94+
len(payload_json),
95+
TEAMS_PAYLOAD_SIZE_LIMIT,
96+
)
97+
card = _truncate_card(card)
98+
payload = _build_payload(card)
99+
51100
response = requests.post(
52101
webhook_url,
53102
json=payload,

0 commit comments

Comments
 (0)