In a Django root URLconf, what signatures must custom handler404 and handler500 views have, and why does the 500 page get so little context?
answer
- module-level variables, not settings
- only the root URLconf counts
- one handler takes no exception
- empty context lessens a second failure
basics
~20 shandler400, 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 sThe 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 linesfrom 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
Recall the four handler names, that they live in the root URLconf, and that handler500 alone takes just the request.
Explain how Django calls each handler, what urls.E007 and urls.E008 catch, and why handlers must set the response status themselves.
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.
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.