Skip to content

Report a refused message instead of swallowing it - #8

Merged
SupraSummus merged 1 commit into
mainfrom
claude/youthful-hypatia-xmc9dp
Sep 14, 2026
Merged

SupraSummus merged 1 commit into
mainfrom
claude/youthful-hypatia-xmc9dp

Conversation

@SupraSummus

@SupraSummus SupraSummus commented Sep 14, 2026

Copy link
Copy Markdown
Member

Problem

GetResponseBackend.__init__ overrode the base constructor without calling it, so fail_silently — which get_connection passes 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.
  • The SMTP backend catches smtplib.SMTPException around the send and re-raises it unless fail_silently; the console backend does the same around Exception. So "raise unless the caller asked otherwise" is the convention in every backend that can fail, not just an idiom.
  • Message construction is deliberately outside that guard — SMTP calls sanitize_address() and message() before the try, so a malformed message raises whatever fail_silently says. message_to_payload() keeps that property here: its ValueErrors for an unknown sender, several recipients or a MIMEBase attachment still propagate.

Change

The flag is honoured. A refusal raises GetResponseSendError carrying the API's error document, with the underlying requests exception chained onto it; fail_silently=True logs the same reason at ERROR instead and leaves the message out of the send count.

The exception is an OSError. Both smtplib.SMTPException, which Django's own backend raises, and requests.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 the fail_silently policy lives in one place in send_messages(). One %s template 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

pytest and flake8 against 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

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
SupraSummus force-pushed the claude/youthful-hypatia-xmc9dp branch from 12fdf2f to ba52a6d Compare September 14, 2026 09:16
@SupraSummus
SupraSummus merged commit 61db56f into main Sep 14, 2026
2 checks passed
@SupraSummus
SupraSummus deleted the claude/youthful-hypatia-xmc9dp branch September 14, 2026 21:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant