skip to content

With DEBUG = False, when does Django email the ADMINS setting about an error, and what does that email contain?

level: middleimportance: should knowfreq 48%

answer

  1. 5xx responses, not 404s
  2. django.request logs at ERROR
  3. subject prefix and IP label
  4. text body; HTML is opt-in

basics

~20 s

Django emails ADMINS when DEBUG is False and a request ends in a 5xx logged at ERROR. The email carries the traceback, request data (GET, POST, cookies, META) and filtered settings; local variables arrive only in the optional HTML attachment.

solid answer

~40 s

When a request produces a 5xx, Django logs it on `django.request` at `ERROR`, and the default logging config sends that record to `AdminEmailHandler`, which is gated to `DEBUG = False`. If `ADMINS` is empty, nothing is sent. The subject looks like `[Django] ERROR (EXTERNAL IP): Internal Server Error: /checkout/`: `EMAIL_SUBJECT_PREFIX`, then the level, then whether `REMOTE_ADDR` is in `INTERNAL_IPS`. The sender is `SERVER_EMAIL`, default `root@localhost`. The text body holds the exception, the traceback, the user, `GET`, `POST`, `FILES`, `COOKIES`, `META` and the settings, all passed through the exception-reporter filter. Local variables only appear in the HTML attachment, which you enable with `include_html=True`. Since 6.0 `ADMINS` should be a list of address strings. 404s go to `MANAGERS`, and only through `BrokenLinkEmailsMiddleware`.

code

python · 8 lines
python
# settings.py
ADMINS = ["[email protected]", '"On-call lead" <[email protected]>']
SERVER_EMAIL = "[email protected]"
EMAIL_SUBJECT_PREFIX = "[shop-prod] "
INTERNAL_IPS = ["10.0.0.5"]  # requests from here are labelled 'internal IP'

# Resulting subject for a crash from a customer's browser:
# [shop-prod] ERROR (EXTERNAL IP): Internal Server Error: /checkout/

go deeper

for a junior

Remember that with DEBUG off, 500 errors are emailed to the addresses in ADMINS, and that the email carries the traceback and request details.

for a middle

Explain the chain from django.request logging at ERROR to AdminEmailHandler, the subject's prefix and internal/EXTERNAL label, SERVER_EMAIL, and why 404s go to MANAGERS via middleware.

for a senior

Decide whether include_html is acceptable given the data your views hold, and check that the report path works and does not mail secrets before you rely on it.

for a principal

Treat admin emails as a fallback channel: set an error-aggregation route through logging, limit who receives raw reports, and keep their content under the same data rules as logs.

## When an error email is sent Django's server-error emails are a by-product of logging. The chain looks like this: 1. A view raises, and the handler turns the exception into a 500 response (see the `DEBUG` split). 2. Django logs the response on the **`django.request`** logger. Its `log_response` helper logs **5xx responses as `ERROR`** and 4xx responses as `WARNING`. 3. Django's default logging configuration has a `mail_admins` handler, level `ERROR`, filtered with `RequireDebugFalse`, class `django.utils.log.AdminEmailHandler`, attached to the `django` logger that `django.request` propagates to. 4. `AdminEmailHandler` builds a report and calls `mail_admins()`, which sends it to every address in **`ADMINS`**. So three conditions must hold: `DEBUG` is `False`, the response status is 500 or above (strictly, anything logged at `ERROR` on a `django.*` logger), and `ADMINS` is not empty. The global default of `ADMINS` is `[]`, and the handler returns early when it is empty, which is why a new project sends nothing. Security events are logged at `ERROR` on `django.security.*` loggers, so a `SuspiciousOperation` such as a disallowed host also produces a report. How the logging config is changed and silenced is a separate topic, and so is the email backend that delivers the message. ## The shape of the email | Part | Where it comes from | |---|---| | Subject prefix | `EMAIL_SUBJECT_PREFIX`, default `'[Django] '` | | Level and origin | `ERROR (internal IP)` if `REMOTE_ADDR` is in `INTERNAL_IPS`, otherwise `ERROR (EXTERNAL IP)` | | Subject text | the log message, e.g. `Internal Server Error: /checkout/`, with CR and LF escaped | | Sender | `SERVER_EMAIL`, default `'root@localhost'` | | Body | the log line, then the plain-text exception report | | Attachment | the HTML version of the technical page, only with `include_html=True` | The plain-text report (`technical_500.txt`) contains: - the exception type and value and the full traceback with the source line of each frame; - the view that raised, and the current user; - the request's `GET`, `POST`, `FILES`, `COOKIES` and `META`; - every setting, with sensitive-looking names starred. What it does **not** contain is the per-frame local variables. Those appear only in the HTML attachment, which `AdminEmailHandler` produces when it is constructed with `include_html=True` (the default is `False`). That choice matters: locals are exactly where a password or a card number sits at the moment of a crash, and the HTML version is the full debug page, sent through your mail infrastructure. ## What the filter does to the email Because `DEBUG` is `False` when these emails go out, the default `SafeExceptionReporterFilter` is **active**. POST fields named with `sensitive_post_parameters` and local variables named with `sensitive_variables` are replaced with stars in the email, and settings, `META` entries and cookies with sensitive names are starred as always. The email is therefore safer than the debug page, but only as safe as your annotations. ## `ADMINS` in current Django - `ADMINS` is a **list of email address strings**: `["[email protected]", "[email protected]"]`. - The old list of `(name, address)` tuples is **deprecated since 6.0** and triggers a `RemovedInDjango70Warning`; Django never used the name part. Write `'"Ops" <[email protected]>'` if you want a display name. - Anything that is not a list or tuple of strings raises `ImproperlyConfigured` when a report is sent. ## 404s are a different mailbox A 404 is logged as a `WARNING`, so it never reaches the `ERROR`-level admin handler. Django can report broken links separately: add `django.middleware.common.BrokenLinkEmailsMiddleware` to `MIDDLEWARE`, and with `DEBUG = False` it mails **`MANAGERS`** about 404s that have a `Referer` header pointing at a different URL. `IGNORABLE_404_URLS` lists regexes to skip. `MANAGERS` has its own default of an empty list, so it must be set explicitly. ## Practical points - A burst of identical 500s produces a burst of identical emails; teams usually route errors into an aggregation tool through logging instead, and keep `ADMINS` as a fallback. - Change `SERVER_EMAIL`: some mail servers reject messages from the default `root@localhost`, and the reports never arrive. - Test the path once in each environment: raise a deliberate error with `DEBUG = False` and confirm the message arrives.

  • Why does a Django project with ADMINS set still not receive an email for a missing page?
    A 404 is logged on `django.request` as a `WARNING`, and the admin email handler only takes `ERROR` records. Broken-link reports need `BrokenLinkEmailsMiddleware` in `MIDDLEWARE`, go to `MANAGERS` rather than `ADMINS`, and are only sent when the request has a `Referer` from a different URL. `IGNORABLE_404_URLS` suppresses known noise.
  • What is the risk of turning on include_html for Django's admin error emails?
    The HTML attachment is the full technical 500 page, including every frame's local variables. Locals hold whatever the code was working on: passwords being verified, card numbers, tokens loaded from the database. The filter still stars what you marked with `sensitive_variables`, but anything unmarked travels by email to every address in `ADMINS`.

saying these in an interview costs you the question

  • Django emails ADMINS about 404 errors by default
  • Error emails are sent even while DEBUG is True
  • ADMINS must be a list of (name, email) tuples
  • The default error email contains every frame's local variables
  • An empty ADMINS list makes Django raise at startup