With DEBUG = False, when does Django email the ADMINS setting about an error, and what does that email contain?
answer
- 5xx responses, not 404s
- django.request logs at ERROR
- subject prefix and IP label
- text body; HTML is opt-in
basics
~20 sDjango 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 sWhen 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# 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
Remember that with DEBUG off, 500 errors are emailed to the addresses in ADMINS, and that the email carries the traceback and request details.
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.
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.
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