skip to content

Resolution & Error Handlers

ROOT_URLCONF, a per-request request.urlconf and resolve() decide which view runs, and handler404 or handler500 answer when none can. Interviewers ask what DEBUG changes about a 404 page.

part ofDjangooverview, primer and where to startread it →
on this pageshow

explore

questions

4

In Django, what happens when a view raises Http404, and how does the response differ between DEBUG=True and DEBUG=False?

level: juniorimportance: must knowfreq 62%

answer

  1. an exception, not a return value
  2. same path as an unmatched URL
  3. developer page lists tried patterns
  4. handler404 renders a 404.html template

basics

~20 s

Raising Http404 abandons the view and Django builds a 404. With DEBUG=True it shows a technical page listing the URL patterns tried; with DEBUG=False it calls handler404, which renders a 404.html template or a plain Not Found page.

solid answer

~40 s

`Http404` from `django.http` is an exception, so raising it, directly or through `get_object_or_404()`, abandons the view and Django's exception handling builds the 404. With `DEBUG = True` you get the technical 404 page: the URLconf used, every pattern tried in order, the exception message and the view that raised it. That is a development aid and an information leak in production. With `DEBUG = False` Django calls the view named by `handler404` in the root URLconf, by default `django.views.defaults.page_not_found`, which renders a `404.html` template if the loaders find one and a minimal "Not Found" page otherwise. The same path runs when no pattern matches, because the resolver raises `Resolver404`, a subclass of `Http404`.

code

python · 7 lines
python
from django.test import TestCase


class BrandedNotFoundTests(TestCase):
    def test_missing_store_uses_branded_page(self):
        response = self.client.get("/stores/no-such-store/")
        self.assertContains(response, "We could not find that page", status_code=404)

go deeper

for a junior

Recall that Http404 is raised, not returned, that get_object_or_404 raises it for you, and that 404.html only appears when DEBUG is False.

for a middle

Explain that an unmatched path raises Resolver404, a subclass of Http404, so both roads reach handler404, and what page_not_found passes to the template.

for a senior

Show the production view: the technical page leaks the URL map, ALLOWED_HOSTS and static serving change with DEBUG, and tests already run with DEBUG off.

for a principal

Treat error pages as part of the product surface: one consistent branded page per status, verified by tests, with no internals leaking through exception messages.

## What `Http404` is `Http404` lives in `django.http` and is an **exception**, not a response class. A view signals "this resource does not exist" by raising it, either directly or through the `get_object_or_404()` shortcut, which calls `get()` and turns the model's `DoesNotExist` into `Http404`: ```python from django.http import Http404 from django.shortcuts import get_object_or_404, render from .models import Store def storefront(request, slug): store = get_object_or_404(Store, slug=slug, is_active=True) if not store.catalog_published: raise Http404("Catalog not published yet") return render(request, "stores/front.html", {"store": store}) ``` Because the view stops at the `raise`, nothing after it runs and the view never returns a response of its own. Django's exception handling around the view catches `Http404` and builds the 404 response for it. Raising is also what makes the 404 composable: a helper three calls deep can raise it without every caller checking a return value. ## Two roads to the same 404 A 404 in Django comes from one of two places, and both end in the same handling: 1. **No URL pattern matched.** The resolver walks the root URLconf's `urlpatterns` in order. When nothing matches `request.path_info`, it raises `Resolver404`, which is defined in `django.urls` and is a **subclass of `Http404`**. 2. **A view raised `Http404`.** The URL resolved and the view ran, but it decided the object does not exist. Since `Resolver404` is an `Http404`, one branded `404.html` covers a mistyped URL and a missing database row alike, and one `handler404` view sees both. ## What `DEBUG` changes | | `DEBUG = True` | `DEBUG = False` | |---|---|---| | Response to `Http404` | the **technical 404 page** | the view named by `handler404` | | Default view | built into `django.views.debug` | `django.views.defaults.page_not_found` | | Shows | the URLconf used, every pattern tried in order, the exception message, the view that raised it | your `404.html`, or a minimal "Not Found" page | | Your `404.html` / `handler404` | never used | used | | Meant for | the developer | the public | The technical page is a development aid: it prints the site's whole URL map, including admin and internal routes, which is exactly what an attacker would like to read. That is one of the reasons `DEBUG` must be `False` in production. One small curiosity: on a brand-new project whose URLconf holds only the admin route, requesting `/` with `DEBUG` on shows Django's welcome page rather than the technical 404. ## Serving a branded 404 page The cheapest branded 404 needs no Python at all: - Create a template named **`404.html`** that the configured template loaders can find (a project-level `templates/` directory is the usual place). - The default `page_not_found` view renders it with two context variables: **`request_path`**, the requested path quoted to block content injection, and **`exception`**, the message passed to `Http404` if it was a string, otherwise the exception's class name. - It renders with the request, so **context processors run**: the page can extend the site's base template and show the navigation and the signed-in user. - Write a custom `handler404` in the root URLconf only when the page needs logic, such as suggesting search results; that view must still return a response with status 404. Because `exception` can reach the page, keep internal details out of `Http404` messages if the template prints `{{ exception }}`. ## Checking the production page locally Turning `DEBUG` off to look at the branded page has side effects worth knowing: - **`ALLOWED_HOSTS` starts to matter.** Its default is an empty list. While `DEBUG` is on, Django quietly allows `.localhost`, `127.0.0.1` and `[::1]`; with `DEBUG` off an empty list rejects every host with a `DisallowedHost` error, answered with a 400. - **Static files disappear.** The `runserver` of `django.contrib.staticfiles` stops serving them when `DEBUG` is off unless started with `--insecure`, so the page may look unstyled. - **Tests already run with `DEBUG = False`.** The test runner switches it off by default, so a test-client request for a missing URL exercises the real production path and can assert on the branded content and the 404 status. ## Mistakes interviewers listen for - **Returning instead of raising.** A helper that returns `HttpResponseNotFound` forces every caller to check its result; raising `Http404` lets it unwind straight to Django's handling. - **Swallowing it.** A broad `except Exception:` around view code also catches `Http404` and can turn a clean 404 into a 500 or, worse, a 200 with an empty page. - **Using it outside views.** In a management command or background job, `get_object_or_404()` raises an `Http404` that nothing turns into a response; call `get()` and handle `DoesNotExist` there. - **Shipping with `DEBUG` on.** The technical page then answers every stray URL on the public site, map included.

  • What context does Django's default page_not_found view give the 404.html template?
    Two variables: `request_path`, the requested path quoted to prevent content injection, and `exception`, the message passed to `Http404` if it was a string, otherwise the exception class name. It renders with the request, so context processors run and the page can extend the base template. Keep internals out of `Http404` messages if the template prints `{{ exception }}`.
  • Why might a Django site return 400 for every request after you set DEBUG = False to preview the 404 page?
    `ALLOWED_HOSTS` defaults to an empty list. While `DEBUG` is on, Django allows `.localhost`, `127.0.0.1` and `[::1]` anyway; once it is off, an empty list rejects every host with a `DisallowedHost` error, which Django answers with a 400. Add the local host names to `ALLOWED_HOSTS`.

The technical 404 page is the building's wiring diagram taped to the door for the electrician; the branded 404.html is the polite sign the public sees once the electrician has gone home and DEBUG is off.

saying these in an interview costs you the question

  • Http404 is a response class you return, like HttpResponseNotFound.
  • A custom 404.html is shown even while DEBUG is True.
  • An unmatched URL and a raised Http404 go through different handling.
  • The technical 404 page is harmless to leave on in production.
  • Switching DEBUG off locally cannot change which hosts Django accepts.
open as a page

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%

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.

open as a page

In Django, what does django.urls.resolve() return for a path, and what can its ResolverMatch tell you?

level: middleimportance: should knowfreq 36%

basics

~20 s

resolve() matches a path against the URLconf and returns a ResolverMatch naming the view, its args and kwargs, url_name, view_name, namespaces and the route template; an unmatched path raises Resolver404. Django stores the same object on request.resolver_match.

open as a page

In a multi-tenant Django project that sets request.urlconf per tenant hostname, what changes for resolution, reverse() and the error handlers, and what breaks outside a request?

level: seniorimportance: nice to knowfreq 22%

basics

~10 s

Django resolves against request.urlconf instead of ROOT_URLCONF and from then on reverse(), {% url %} and the handler404/500 lookup follow it; code outside a request falls back to ROOT_URLCONF unless urlconf= is passed.

open as a page