Report a refused message instead of swallowing it - #8
Merged
Merged
Conversation
The backend accepted `fail_silently` from `get_connection` and dropped it, so a message the API rejected came back only as a send count one short: callers had no way to learn why, and the reason — GetResponse's own error document, which says whether asking again could work — reached the log and nowhere else. Honouring the flag is what a Django backend owes its callers, and it makes the two halves alternatives rather than a log that hopes someone is reading it. A 2xx that is not a 201 was the worse case: it creates no message and was reported nowhere at all, not even to the log. The refusal is an `OSError`, so generic handling keeps working across backends: `smtplib.SMTPException`, which Django's own SMTP backend raises, and the `requests.RequestException` wrapped here are both subclasses of it already. The refusal the log still carries names the reason through an argument rather than inside the message, so a monitor grouping by message sees one recurring event rather than a new one per call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VFDRdudEpf5nC61rJZZ8dz
SupraSummus
force-pushed
the
claude/youthful-hypatia-xmc9dp
branch
from
September 14, 2026 09:16
12fdf2f to
ba52a6d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
GetResponseBackend.__init__overrode the base constructor without calling it, sofail_silently— whichget_connectionpasses in on every send — was accepted and dropped. The backend therefore never raised, whatever the caller asked for.A message the API rejected came back only as a send count one short. The reason, GetResponse's own error document, which is what says whether asking again could work, reached the log and nowhere else, so a caller had no way to learn why nothing was sent.
A 2xx that is not a 201 was the worse case: it creates no message, and it was reported nowhere at all, not even to the log.
What the protocol actually is
Checked against Django 5.2's own backends rather than from memory:
BaseEmailBackend.__init__(self, fail_silently=False, **kwargs)stores the flag;send_messages()returns the number of messages sent.smtplib.SMTPExceptionaround the send and re-raises it unlessfail_silently; the console backend does the same aroundException. So "raise unless the caller asked otherwise" is the convention in every backend that can fail, not just an idiom.sanitize_address()andmessage()before thetry, so a malformed message raises whateverfail_silentlysays.message_to_payload()keeps that property here: itsValueErrors for an unknown sender, several recipients or aMIMEBaseattachment still propagate.Change
The flag is honoured. A refusal raises
GetResponseSendErrorcarrying the API's error document, with the underlyingrequestsexception chained onto it;fail_silently=Truelogs the same reason atERRORinstead and leaves the message out of the send count.The exception is an
OSError. Bothsmtplib.SMTPException, which Django's own backend raises, andrequests.RequestException, which this wraps, are already subclasses of it, so a caller that handles mail failures generically keeps working and does not need to know which backend it got.Raising and logging are alternatives rather than both, so a refusal is never reported twice.
_send_message()either returns an id or raises, and thefail_silentlypolicy lives in one place insend_messages(). One%stemplate backs both the exception message and the log record, so the wording cannot drift; the reason goes into the log as an argument, so a monitor grouping by message sees one recurring event rather than a new one per call.Compatibility
This changes behaviour for every caller: code that relied on the backend never raising will now see exceptions unless it passes
fail_silently=True. A batch stops at the first refusal, as the SMTP backend's does. Version bumped to 0.3.0 and the README documents the new contract.Verification
pytestandflake8against both Django versions in the CI matrix (4.2.30 and 5.2.17). Tests cover a transport failure, a rejected request, the non-201 success, and both silenced paths.🤖 Generated with Claude Code
https://claude.ai/code/session_01VFDRdudEpf5nC61rJZZ8dz