Skip to content

Commit 335de96

Browse files
committed
SendGrid: Handle non-string data in legacy substitutions
SendGrid returns a cryptic "Bad Request" error (with no other details) when legacy substitutions values are anything other than string or null. (This only applies to substitutions, for legacy templates and on-the-fly merge fields.) Convert numeric types to string (per Anymail's documented `merge_data` behavior), and raise an AnymailSerializationError for other unsupported substitutions values. Fixes #420
1 parent 6acdf36 commit 335de96

File tree

3 files changed

+78
-2
lines changed

3 files changed

+78
-2
lines changed

CHANGELOG.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ Fixes
5656
* **Postmark:** Fix an error in inbound handling with long email address display
5757
names that include non-ASCII characters.
5858

59+
* **SendGrid:** Improve handling of non-string values in ``merge_data`` when using
60+
legacy templates or inline merge fields. To avoid a confusing SendGrid API error
61+
message, Anymail now converts numeric merge data values to strings, but will raise
62+
an AnymailSerializationError for other non-string data in SendGrid substitutions.
63+
(SendGrid's newer *dynamic* transactional templates do not have this limitation.)
64+
(Thanks to `@PlusAsh`_ for reporting the issue.)
65+
5966
Other
6067
~~~~~
6168

@@ -1798,6 +1805,7 @@ Features
17981805
.. _@mwheels: https://github.com/mwheels
17991806
.. _@nuschk: https://github.com/nuschk
18001807
.. _@originell: https://github.com/originell
1808+
.. _@PlusAsh: https://github.com/PlusAsh
18011809
.. _@puru02: https://github.com/puru02
18021810
.. _@RignonNoel: https://github.com/RignonNoel
18031811
.. _@rodrigondec: https://github.com/rodrigondec

anymail/backends/sendgrid.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33

44
from requests.structures import CaseInsensitiveDict
55

6-
from ..exceptions import AnymailConfigurationError, AnymailWarning
6+
from ..exceptions import (
7+
AnymailConfigurationError,
8+
AnymailSerializationError,
9+
AnymailWarning,
10+
)
711
from ..message import AnymailRecipientStatus
812
from ..utils import BASIC_NUMERIC_TYPES, Mapping, get_anymail_setting, update_deep
913
from .base_requests import AnymailRequestsBackend, RequestsPayload
@@ -159,6 +163,20 @@ def convert_dynamic_template_data_to_legacy_substitutions(self):
159163
"""
160164
Change personalizations[...]['dynamic_template_data'] to ...['substitutions]
161165
"""
166+
167+
def transform_substitution_value(value):
168+
# SendGrid substitutions must be string or null, or SendGrid issues the
169+
# cryptic error `{"field": null, "message": "Bad Request", "help": null}`.
170+
# Anymail will convert numbers; treat anything else as an error.
171+
if isinstance(value, str) or value is None:
172+
return value
173+
if isinstance(value, BASIC_NUMERIC_TYPES):
174+
return str(value)
175+
raise AnymailSerializationError(
176+
"SendGrid legacy substitutions require string values."
177+
f" Don't know how to handle {type(value).__name__}."
178+
)
179+
162180
merge_field_format = self.merge_field_format or "{}"
163181

164182
all_merge_fields = set()
@@ -171,7 +189,7 @@ def convert_dynamic_template_data_to_legacy_substitutions(self):
171189
# Convert dynamic_template_data keys for substitutions,
172190
# using merge_field_format
173191
personalization["substitutions"] = {
174-
merge_field_format.format(field): data
192+
merge_field_format.format(field): transform_substitution_value(data)
175193
for field, data in dynamic_template_data.items()
176194
}
177195
all_merge_fields.update(dynamic_template_data.keys())

tests/test_sendgrid_backend.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,56 @@ def test_legacy_warn_if_no_global_merge_field_delimiters(self):
831831
with self.assertWarnsRegex(AnymailWarning, r"SENDGRID_MERGE_FIELD_FORMAT"):
832832
self.message.send()
833833

834+
def test_legacy_data_conversion(self):
835+
"""
836+
SendGrid requires string (or null) substitution values.
837+
Anymail will convert numbers.
838+
"""
839+
# (Legacy substitutions path because not using dynamic template id.)
840+
self.message.to = ["alice@example.com"]
841+
self.message.merge_data = {
842+
"alice@example.com": {
843+
":integer": 1,
844+
":float": 1.0,
845+
":null": None,
846+
}
847+
}
848+
self.message.send()
849+
data = self.get_api_call_json()
850+
self.assertEqual(
851+
data["personalizations"][0]["substitutions"],
852+
{
853+
":integer": "1",
854+
":float": "1.0",
855+
":null": None,
856+
},
857+
)
858+
859+
def test_legacy_unsupported_string_conversion(self):
860+
"""
861+
SendGrid requires string substitution values (and issues a cryptic
862+
error otherwise). Anymail treats non-convertible data as an error.
863+
"""
864+
# (Legacy substitutions path because not using dynamic template id.)
865+
self.message.to = ["alice@example.com"]
866+
cases = [
867+
["array"],
868+
{"dict": 1},
869+
Decimal("1.0"),
870+
]
871+
for case in cases:
872+
with self.subTest(case=case):
873+
self.message.merge_data = {
874+
"alice@example.com": {
875+
":value": case,
876+
}
877+
}
878+
with self.assertRaisesMessage(
879+
AnymailSerializationError,
880+
"SendGrid legacy substitutions require string values",
881+
):
882+
self.message.send()
883+
834884
def test_merge_metadata(self):
835885
self.message.to = ["alice@example.com", "Bob <bob@example.com>"]
836886
self.message.merge_metadata = {

0 commit comments

Comments
 (0)