Mailjet: Don't try to parse non-JSON responses as JSON.

Fixes #409.
This commit is contained in:
Mike Edmunds
2024-12-10 12:28:47 -08:00
parent c7f7428b7a
commit 8def0bdc06
3 changed files with 55 additions and 38 deletions

View File

@@ -43,6 +43,8 @@ Fixes
* **Mailjet:** Avoid a Mailjet API error when sending an inline image without a * **Mailjet:** Avoid a Mailjet API error when sending an inline image without a
filename. (Anymail now substitutes ``"attachment"`` for the missing filename.) filename. (Anymail now substitutes ``"attachment"`` for the missing filename.)
(Thanks to `@chickahoona`_ for reporting the issue.) (Thanks to `@chickahoona`_ for reporting the issue.)
* **Mailjet:** Fix a JSON parsing error on Mailjet 429 "too many requests" API
responses. (Thanks to `@rodrigondec`_ for reporting the issue.)
v12.0 v12.0
@@ -1779,6 +1781,7 @@ Features
.. _@originell: https://github.com/originell .. _@originell: https://github.com/originell
.. _@puru02: https://github.com/puru02 .. _@puru02: https://github.com/puru02
.. _@RignonNoel: https://github.com/RignonNoel .. _@RignonNoel: https://github.com/RignonNoel
.. _@rodrigondec: https://github.com/rodrigondec
.. _@sblondon: https://github.com/sblondon .. _@sblondon: https://github.com/sblondon
.. _@scur-iolus: https://github.com/scur-iolus .. _@scur-iolus: https://github.com/scur-iolus
.. _@sdarwin: https://github.com/sdarwin .. _@sdarwin: https://github.com/sdarwin

View File

@@ -34,7 +34,10 @@ class EmailBackend(AnymailRequestsBackend):
return MailjetPayload(message, defaults, self) return MailjetPayload(message, defaults, self)
def raise_for_status(self, response, payload, message): def raise_for_status(self, response, payload, message):
if 400 <= response.status_code <= 499: content_type = (
response.headers.get("content-type", "").split(";")[0].strip().lower()
)
if 400 <= response.status_code <= 499 and content_type == "application/json":
# Mailjet uses 4xx status codes for partial failure in batch send; # Mailjet uses 4xx status codes for partial failure in batch send;
# we'll determine how to handle below in parse_recipient_status. # we'll determine how to handle below in parse_recipient_status.
return return

View File

@@ -10,6 +10,7 @@ from django.test import SimpleTestCase, override_settings, tag
from anymail.exceptions import ( from anymail.exceptions import (
AnymailAPIError, AnymailAPIError,
AnymailRequestsAPIError,
AnymailSerializationError, AnymailSerializationError,
AnymailUnsupportedFeature, AnymailUnsupportedFeature,
) )
@@ -690,43 +691,41 @@ class MailjetBackendAnymailFeatureTests(MailjetBackendMockAPITestCase):
# Mailjet's v3.1 API will partially fail a batch send, allowing valid emails # Mailjet's v3.1 API will partially fail a batch send, allowing valid emails
# to go out. The API response doesn't identify the failed email addresses; # to go out. The API response doesn't identify the failed email addresses;
# make sure we represent them correctly in the anymail_status. # make sure we represent them correctly in the anymail_status.
response_content = json.dumps( response_data = {
{ "Messages": [
"Messages": [ {
{ "Status": "success",
"Status": "success", "CustomID": "",
"CustomID": "", "To": [
"To": [ {
{ "Email": "to-good@example.com",
"Email": "to-good@example.com", "MessageUUID": "556e896a-e041-4836-bb35-8bb75ee308c5",
"MessageUUID": "556e896a-e041-4836-bb35-8bb75ee308c5", "MessageID": 12345678901234500,
"MessageID": 12345678901234500, "MessageHref": "https://api.mailjet.com/v3/REST"
"MessageHref": "https://api.mailjet.com/v3/REST" "/message/12345678901234500",
"/message/12345678901234500", }
} ],
], "Cc": [],
"Cc": [], "Bcc": [],
"Bcc": [], },
}, {
{ "Errors": [
"Errors": [ {
{ "ErrorIdentifier": "f480a5a2-0334-4e08"
"ErrorIdentifier": "f480a5a2-0334-4e08" "-b2b7-f372ce5669e0",
"-b2b7-f372ce5669e0", "ErrorCode": "mj-0013",
"ErrorCode": "mj-0013", "StatusCode": 400,
"StatusCode": 400, "ErrorMessage": '"invalid@123.4" is an invalid'
"ErrorMessage": '"invalid@123.4" is an invalid' " email address.",
" email address.", "ErrorRelatedTo": ["To[0].Email"],
"ErrorRelatedTo": ["To[0].Email"], }
} ],
], "Status": "error",
"Status": "error", },
}, ]
] }
}
).encode("utf-8")
# Mailjet uses 400 for partial success: # Mailjet uses 400 for partial success:
self.set_mock_response(raw=response_content, status_code=400) self.set_mock_response(json_data=response_data, status_code=400)
msg = mail.EmailMessage( msg = mail.EmailMessage(
"Subject", "Subject",
"Message", "Message",
@@ -751,7 +750,7 @@ class MailjetBackendAnymailFeatureTests(MailjetBackendMockAPITestCase):
msg.anymail_status.recipients["invalid@123.4"].message_id, None msg.anymail_status.recipients["invalid@123.4"].message_id, None
) )
self.assertEqual(msg.anymail_status.message_id, {"12345678901234500", None}) self.assertEqual(msg.anymail_status.message_id, {"12345678901234500", None})
self.assertEqual(msg.anymail_status.esp_response.content, response_content) self.assertEqual(msg.anymail_status.esp_response.json(), response_data)
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
def test_send_failed_anymail_status(self): def test_send_failed_anymail_status(self):
@@ -764,6 +763,18 @@ class MailjetBackendAnymailFeatureTests(MailjetBackendMockAPITestCase):
self.assertEqual(self.message.anymail_status.recipients, {}) self.assertEqual(self.message.anymail_status.recipients, {})
self.assertIsNone(self.message.anymail_status.esp_response) self.assertIsNone(self.message.anymail_status.esp_response)
def test_non_json_error(self):
"""429 (and other?) errors don't have a json payload"""
self.set_mock_response(
status_code=429, raw=b"Rate limit exceeded", content_type="text/html"
)
with self.assertRaisesRegex(
AnymailRequestsAPIError,
r"^Mailjet API response 429 .*Rate limit exceeded.*",
) as cm:
self.message.send()
self.assertNotIn("Invalid JSON", str(cm.exception))
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
def test_send_unparsable_response(self): def test_send_unparsable_response(self):
""" """