-
Notifications
You must be signed in to change notification settings - Fork 210
Truncate oversized Teams webhook payloads to prevent HTTP 413 errors #2160
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f7b1130
1d7dbdc
59c6b7b
97195a2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import json | ||
| from datetime import datetime, timezone | ||
| from http import HTTPStatus | ||
| from typing import Any, Optional | ||
| from typing import Any, Dict, List, Optional | ||
|
|
||
| import requests | ||
| from ratelimit import limits, sleep_and_retry | ||
|
|
@@ -25,6 +26,7 @@ | |
|
|
||
| Channel: TypeAlias = Optional[str] | ||
| ONE_SECOND = 1 | ||
| TEAMS_PAYLOAD_SIZE_LIMIT = 27 * 1024 | ||
|
|
||
|
|
||
| class TeamsWebhookHttpError(MessagingIntegrationError): | ||
|
|
@@ -36,8 +38,8 @@ def __init__(self, response: requests.Response): | |
| ) | ||
|
|
||
|
|
||
| def send_adaptive_card(webhook_url: str, card: dict) -> requests.Response: | ||
| payload = { | ||
| def _build_payload(card: dict) -> dict: | ||
| return { | ||
| "type": "message", | ||
| "attachments": [ | ||
| { | ||
|
|
@@ -48,6 +50,60 @@ def send_adaptive_card(webhook_url: str, card: dict) -> requests.Response: | |
| ], | ||
| } | ||
|
|
||
|
|
||
| def _truncation_notice_item() -> Dict[str, Any]: | ||
| return { | ||
| "type": "TextBlock", | ||
| "text": "_... Content truncated due to message size limits._", | ||
| "wrap": True, | ||
| "isSubtle": True, | ||
| } | ||
|
|
||
|
|
||
| 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 | ||
|
Comment on lines
+63
to
+91
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minimal fallback can still exceed limit if non-body fields are large.
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 Replace with (RUF005) [warning] 88-88: Consider Replace with (RUF005) 🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| def send_adaptive_card(webhook_url: str, card: dict) -> requests.Response: | ||
| payload = _build_payload(card) | ||
| payload_json = json.dumps(payload) | ||
| if len(payload_json) > TEAMS_PAYLOAD_SIZE_LIMIT: | ||
| logger.warning( | ||
| "Teams webhook payload size (%d bytes) exceeds limit (%d bytes), " | ||
| "truncating card body", | ||
| len(payload_json), | ||
| TEAMS_PAYLOAD_SIZE_LIMIT, | ||
| ) | ||
| card = _truncate_card(card) | ||
| payload = _build_payload(card) | ||
|
|
||
| response = requests.post( | ||
| webhook_url, | ||
| json=payload, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.