Skip to content

Commit e0fef90

Browse files
committed
fix: Handle payload too large size and telemetry
1 parent dff848a commit e0fef90

4 files changed

Lines changed: 87 additions & 3 deletions

File tree

mailjet_rest/builders.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import base64
6+
import json
67
import mimetypes
78
import sys
89
from typing import TYPE_CHECKING
@@ -182,7 +183,7 @@ def build(self) -> SendV31Message:
182183
if "TextPart" not in self._msg and "HTMLPart" not in self._msg and "TemplateID" not in self._msg:
183184
msg = "Message validation failed: TextPart, HTMLPart, or TemplateID is required."
184185
raise ValueError(msg)
185-
if "Variables" in self._msg and sys.getsizeof(str(self._msg["Variables"])) > 1024 * 1024:
186+
if "Variables" in self._msg and len(json.dumps(self._msg["Variables"])) > 1024 * 1024:
186187
msg = "Security Violation: Variables payload too large."
187188
raise ValueError(msg)
188189

mailjet_rest/client.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -601,9 +601,11 @@ def _extract_telemetry(data: Any, _headers: dict[str, str] | None) -> tuple[str,
601601
structured_data = {}
602602
with suppress(Exception):
603603
if isinstance(data, dict):
604-
# Only extract fields that are mathematically non-PII
604+
messages = data.get("Messages", [{}])
605+
target_dict = messages[0] if isinstance(messages, list) and messages else data
606+
605607
for field in _ALLOWED_TRACE_FIELDS:
606-
if val := data.get(field):
608+
if val := target_dict.get(field) or data.get(field):
607609
clean_val = SecurityGuard.sanitize_log_trace(val)
608610
trace_ctx.append(f"{field}={clean_val}")
609611
structured_data[f"mailjet.{field.lower()}"] = clean_val

tests/unit/test_builders.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import pytest
2+
from mailjet_rest.builders import MessageBuilder
3+
4+
5+
def test_message_builder_variables_size_limit() -> None:
6+
"""Verify blocking of excessively large Variables objects to prevent Out-Of-Memory (OOM) errors."""
7+
builder = MessageBuilder()
8+
9+
# Create a dictionary that exceeds 1MB when JSON-serialized
10+
large_payload = {"huge_key": "x" * (1024 * 1024 + 100)}
11+
12+
builder._msg = {
13+
"From": {"Email": "sender@example.com", "Name": "System"},
14+
"To": [{"Email": "recipient@example.com"}],
15+
"TextPart": "Hello",
16+
"Variables": large_payload,
17+
}
18+
19+
with pytest.raises(ValueError, match="Security Violation: Variables payload too large"):
20+
builder.build()
21+
22+
23+
def test_message_builder_variables_safe_size() -> None:
24+
"""Verify that a valid, safe-sized Variables object passes validation without errors."""
25+
builder = MessageBuilder()
26+
27+
builder._msg = {
28+
"From": {"Email": "sender@example.com", "Name": "System"},
29+
"To": [{"Email": "recipient@example.com"}],
30+
"TextPart": "Hello",
31+
"Variables": {"small_key": "safe_value"},
32+
}
33+
34+
result = builder.build()
35+
assert "Variables" in result
36+
assert "From" in result
37+
assert "To" in result
38+
assert "TextPart" in result

tests/unit/test_client.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,3 +815,46 @@ def test_property_config_invariants(url: str, audit_flag: bool) -> None:
815815
# Verify configuration normalizes trailing slashes properly
816816
assert cfg.api_url == f"{url}/"
817817
assert cfg.enable_security_audit is audit_flag
818+
819+
820+
# ==========================================
821+
# 10. Telemetry
822+
# ==========================================
823+
824+
825+
def test_extract_telemetry_v3_root_level() -> None:
826+
"""Verify telemetry extraction from the root level of the payload (API v3)."""
827+
# Use exact keys monitored within the allowed set: CustomID and TemplateID
828+
payload = {"CustomID": "trace-root-123", "TemplateID": 112233}
829+
trace_str, struct_data = Client._extract_telemetry(payload, None)
830+
831+
assert "CustomID=trace-root-123" in trace_str
832+
assert "TemplateID=112233" in trace_str
833+
assert struct_data["mailjet.customid"] == "trace-root-123"
834+
assert struct_data["mailjet.templateid"] == "112233"
835+
836+
def test_extract_telemetry_v31_nested_level() -> None:
837+
"""Verify telemetry extraction from the nested Messages array (API v3.1)."""
838+
payload = {
839+
"Messages": [
840+
{"CustomID": "trace-nested-456", "TemplateID": 98765}
841+
]
842+
}
843+
trace_str, struct_data = Client._extract_telemetry(payload, None)
844+
845+
assert "CustomID=trace-nested-456" in trace_str
846+
assert "TemplateID=98765" in trace_str
847+
assert struct_data["mailjet.customid"] == "trace-nested-456"
848+
assert struct_data["mailjet.templateid"] == "98765"
849+
850+
def test_extract_telemetry_safe_fallback() -> None:
851+
"""Verify safe handling of invalid or empty data (no exceptions raised)."""
852+
# Test empty array
853+
trace_str, struct_data = Client._extract_telemetry([], None)
854+
assert trace_str == ""
855+
assert struct_data == {}
856+
857+
# Test missing Messages payload
858+
trace_str, struct_data = Client._extract_telemetry({"Messages": []}, None)
859+
assert trace_str == ""
860+
assert struct_data == {}

0 commit comments

Comments
 (0)