skip to content

In a Django root URLconf, what signatures must custom handler404 and handler500 views have, and why does the 500 page get so little context?

level: middleimportance: should knowfreq 44%

answer

  1. module-level variables, not settings
  2. only the root URLconf counts
  3. one handler takes no exception
  4. empty context lessens a second failure

basics

~20 s

handler400, handler403 and handler404 take (request, exception); handler500 takes only (request). Django renders the default 500.html with an empty context because whatever broke the view may also break a template that uses the request or database.

solid answer

~40 s

The four variables `handler400`, `handler403`, `handler404` and `handler500` are read from the root URLconf module and may hold a callable or a dotted path; setting them in an included app URLconf does nothing. The first three are called with `(request, exception)`; `handler500` gets only `(request)`, because it runs on the uncaught-exception path. `manage.py check` flags a wrong arity as `urls.E007` and an unimportable path as `urls.E008`. The default `server_error` renders `500.html` with an empty context, no request and no context processors, so the page cannot fail for the same reason the view did; a branded 500 page should stay self-contained. Custom handlers must also set the status themselves: `render()` without `status=404` sends 200.

code

python · 23 lines
python
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from django.test import SimpleTestCase, override_settings
from django.urls import path


def forbidden_view(request):
    raise PermissionDenied


def branded_forbidden(request, exception):
    return HttpResponse("Members only", status=403)


urlpatterns = [path("vault/", forbidden_view)]
handler403 = branded_forbidden


@override_settings(ROOT_URLCONF=__name__)
class Handler403Tests(SimpleTestCase):
    def test_branded_403(self):
        response = self.client.get("/vault/")
        self.assertContains(response, "Members only", status_code=403)

go deeper

for a junior

Recall the four handler names, that they live in the root URLconf, and that handler500 alone takes just the request.

for a middle

Explain how Django calls each handler, what urls.E007 and urls.E008 catch, and why handlers must set the response status themselves.

for a senior

Design error views that cannot fail: a self-contained 500 page rendered without the request, a 404 that returns status 404, and tests that pin both.

for a principal

Weigh how much a branded error page may depend on the running system against the risk that the page fails alongside the outage it reports.

## The four handler variables Django names its error views through four module-level variables. They are **not settings**: they are read from the URLconf module that Django is resolving against, normally the one named by `ROOT_URLCONF`. | Variable | Triggered by | Default view | Signature | |---|---|---|---| | `handler400` | `BadRequest`, an unhandled `SuspiciousOperation`, a multipart parse error | `django.views.defaults.bad_request` | `(request, exception)` | | `handler403` | `PermissionDenied` | `django.views.defaults.permission_denied` | `(request, exception)` | | `handler404` | `Http404`, including `Resolver404` when no pattern matched | `django.views.defaults.page_not_found` | `(request, exception)` | | `handler500` | any other exception escaping the view | `django.views.defaults.server_error` | `(request)` | Each value may be a callable or a dotted import path string. Setting them in an app's included `urls.py` does nothing: Django looks them up on the root URLconf module only, and falls back to its own defaults when that module does not define one. ```python # config/urls.py (the module ROOT_URLCONF points at) from django.urls import include, path urlpatterns = [ path("shop/", include("shop.urls")), ] handler404 = "pages.views.branded_not_found" handler500 = "pages.views.branded_server_error" ``` ## Why the signatures differ `handler400`, `handler403` and `handler404` are called as `callback(request, exception=exc)`: the exception is known and may carry a useful message. `handler500` is called as `callback(request)` from the uncaught-exception path, with no exception argument, because the failure is by definition something nobody anticipated and its details belong in logs, not on a page. The system check framework guards the arity. `manage.py check`, which `runserver` also runs at startup, binds each handler of the root URLconf to the expected number of arguments and reports: - **`urls.E007`** when a handler does not take the correct number of arguments; - **`urls.E008`** when the dotted path cannot be imported. ## Why the 500 page gets so little The default `server_error` view renders `500.html` with an **empty context**: no request, no context processors, no variables. The docs give the reason: it lessens the chance of a second error. Consider what a 500 usually means: the database is unreachable, a template tag is broken, a setting is wrong. A 500 template that extends the base layout, runs the auth context processor, reads the current user or queries a site-wide banner can fail for the same reason the view failed. So a branded 500 page should be: 1. a self-contained template with inline or absolute-URL styles; 2. free of `{% url %}` calls, template tags that hit the database, and `request.user`; 3. rendered without a request, for example with `render_to_string()` and no request argument. ```python # pages/views.py from django.http import HttpResponseServerError from django.shortcuts import render from django.template.loader import render_to_string def branded_not_found(request, exception): return render(request, "errors/404.html", {"path": request.path}, status=404) def branded_server_error(request): return HttpResponseServerError(render_to_string("errors/500.html")) ``` The 404 handler, by contrast, can safely render with the request: the site is healthy, the page just does not exist. ## Details that bite - **The status is yours to set.** Django returns whatever the handler returns. `render(request, "404.html")` without `status=404` sends **200 OK**, a soft 404 that search engines index and monitoring never counts. - **A crashing handler escalates.** If a `handler404` itself raises, Django treats that as an uncaught exception and answers with the 500 handler. - **`DEBUG` bypasses most of them.** With `DEBUG = True` Django shows the technical 404 page instead of `handler404`, the technical 500 page instead of `handler500`, and a technical page with status 400 for `BadRequest` and `SuspiciousOperation`. `PermissionDenied`, however, goes to `handler403` whatever `DEBUG` says. - **CSRF failures are separate.** A rejected CSRF token is answered by the view in `CSRF_FAILURE_VIEW`, not by `handler403`. - **Testing needs the root URLconf.** Error-handler tests typically point `ROOT_URLCONF` at a test module with `override_settings`, because the handlers are read from the root module. ## What each default passes to its template The defaults differ in how much of the exception they expose, and a custom handler should be at least as careful: - **`page_not_found`** passes `request_path` and `exception` (the `Http404` message if it was a string, otherwise the class name) and renders with the request. - **`permission_denied`** passes `exception` as the string form of the `PermissionDenied` that was raised. - **`bad_request`** passes nothing about the exception, because its message might contain sensitive details such as filesystem paths. - **`server_error`** passes nothing at all and renders without the request. A handler that prints `str(exception)` on a public page publishes whatever message a developer wrote for a log line, so treat exception text as internal unless it was written for users.

  • In Django, what happens if a custom handler404 view itself raises an exception?
    Django catches it and treats it as an uncaught exception, so the request is answered by the 500 path: the technical 500 page with `DEBUG = True`, otherwise `handler500`. That is one more reason to keep error views simple and free of queries that could fail.
  • Which Django error handlers still run when DEBUG = True?
    `handler403` does: `PermissionDenied` goes to it whatever `DEBUG` says. `handler404` and `handler500` are replaced by the technical 404 and 500 pages, and `BadRequest` or `SuspiciousOperation` gets a technical page with status 400. CSRF failures never use `handler403`; they go to the view in `CSRF_FAILURE_VIEW`.

saying these in an interview costs you the question

  • handler404 can be set in any app's urls.py and the innermost one wins.
  • handler500 receives the exception as its second argument.
  • Django forces status 404 on whatever handler404 returns.
  • A 500 template should extend the base layout and show the signed-in user.
  • Error handlers are configured as settings in settings.py.