How does Django's View.dispatch() choose a handler method, and when does a class-based view answer 405 Method Not Allowed?
answer
- lowercased request method
- an allow-list on the class
- getattr with a fallback
- HEAD borrows get()
- Allow header built from both
basics
~20 sView.dispatch() lowercases request.method and, if it is in http_method_names, calls the method of that name. A name outside the list, or one with no handler defined, goes to http_method_not_allowed(), which returns 405 with an Allow header.
solid answer
~40 s`dispatch()` computes `request.method.lower()`. If that name is in `http_method_names` it does `getattr(self, method, self.http_method_not_allowed)`; otherwise it goes straight to `http_method_not_allowed()`. So a 405 comes from two cases: a verb the class does not list (for example `PROPFIND`), or a listed verb with no handler (a `post` when only `get()` exists, or `trace`, which is listed by default but not implemented). `http_method_not_allowed()` returns `HttpResponseNotAllowed`, whose `Allow` header names every verb that is both listed and defined on the instance, and logs a warning to the `django.request` logger. `HEAD` works on a view with `get()` (while `head` stays in `http_method_names`) because `setup()` aliases `head` to `get`, and `OPTIONS` is answered by `View.options()`, which returns an empty 200 with the same `Allow` list.
code
python · 17 linesfrom django.http import HttpResponse
from django.views import View
class ReadOnlyReportView(View):
http_method_names = ["get", "head", "options"]
def get(self, request, *args, **kwargs):
return HttpResponse("report")
def post(self, request, *args, **kwargs): # never reached: not in the list
return HttpResponse("created")
# GET -> 200 from get()
# HEAD -> 200 from get() via the setup() alias
# OPTIONS -> 200, Allow: GET, HEAD, OPTIONS
# POST -> 405, Allow: GET, HEAD, OPTIONSgo deeper
Remember that GET goes to get() and POST to post(), and that an unsupported verb gets 405 rather than 404 or 500.
Walk through dispatch(): lowercase, allow-list check, getattr with the http_method_not_allowed fallback, plus the HEAD alias and the built-in options().
Explain how Allow is computed from list and instance, why http_method_names = ["get"] also kills HEAD and OPTIONS, and when overriding http_method_not_allowed is reasonable.
Decide how strictly read-only views should declare their verbs, weighing explicit allow-lists against relying on which handlers happen to exist.
## The dispatch algorithm Every Django class-based view inherits `dispatch()` from `django.views.View`. Its job is to map the HTTP method to a Python method on the view instance. The whole algorithm fits in three steps: 1. Compute `method = request.method.lower()`, so `GET` becomes `get`. 2. If `method` is in the class attribute **`http_method_names`**, look up `getattr(self, method, self.http_method_not_allowed)`. 3. If it is not in the list, use `self.http_method_not_allowed` directly. Then call the chosen handler with `(request, *args, **kwargs)`. `http_method_names` is therefore an **allow-list**, and a matching method on the class is the second condition. Both must hold for a handler to run. The default list is: ```python ["get", "post", "put", "patch", "delete", "head", "options", "trace"] ``` ## When the answer is 405 `http_method_not_allowed()` is the fallback in both branches. It returns an `HttpResponseNotAllowed` (status **405**) with an empty body and logs a warning through `django.request` (Django logs 4xx responses as warnings). The cases that reach it: | Request | View defines | Outcome | |---|---|---| | `GET` | `get()` | `get()` runs | | `HEAD` | `get()` only | `get()` runs, because `setup()` set `self.head = self.get` | | `POST` | `get()` only | 405: `post` is listed but not defined | | `TRACE` | nothing special | 405: listed by default, but `View` has no `trace()` | | `PROPFIND` | anything | 405: not in `http_method_names` at all | | `OPTIONS` | nothing special | 200 from the built-in `View.options()` | A 405 is not a 404: the URL pattern matched and the view ran; only the verb was refused. (How a router in general decides between 404 and 405 is a framework-neutral topic; in Django the URL resolver ignores the method entirely and leaves the decision to the view.) One caveat when testing by hand: middleware runs before the view, so an unsafe request without a CSRF token can be rejected with 403 before `dispatch()` is ever reached. The table describes what the view itself returns. ## The Allow header Both the 405 response and the `OPTIONS` response list methods through the private helper `_allowed_methods()`, which returns the **uppercased** names that are in `http_method_names` **and** exist on the instance. Because it checks the instance after `setup()`, a view with only `get()` advertises `GET, HEAD, OPTIONS`: `head` exists thanks to the alias and `options` is inherited from `View`. `View.options()` returns an empty `HttpResponse` (status 200) with `Allow` set and `Content-Length: 0`. It is a real handler, so it obeys the same allow-list rule as everything else. ## Narrowing or widening the verbs - **Narrowing** — set `http_method_names = ["get", "head", "options"]` on a read-only view and `POST`, `PUT` and friends return 405 even if a mixin happens to define `post()`. Beware of `http_method_names = ["get"]`: it also removes `head` and `options`, so `HEAD` and `OPTIONS` both become 405 even though handlers for them exist. - **Widening** — to accept a verb that is not in the default list, add its lowercase name to `http_method_names` **and** define a method with that name. Either half alone still produces 405. - **Removing a handler per route** — you cannot pass `get=None` to `as_view()`; it rejects HTTP method names as keywords. Subclass or override `http_method_names` instead. ## Customising the refusal `http_method_not_allowed()` is a normal method, so a view can override it — for example, to render a friendlier page for browsers or to add a structured body for an API client — as long as it still returns a 405 with an accurate `Allow` header. Returning 404 instead hides the resource's existence but breaks clients that rely on `Allow`. ## Testing the verb handling A view's method policy is easy to pin down in tests with Django's test `Client`, which does not enforce CSRF checks by default: - `self.client.post(url)` on a GET-only view should give `response.status_code == 405`. - `response.headers["Allow"]` should list exactly the verbs you intend to expose. - `self.client.options(url)` should return 200 with the same `Allow` value, or 405 if `options` was deliberately left out of `http_method_names`. Pinning `Allow` in a test catches the case where a mixin quietly adds a `post()` handler to a view that was meant to be read-only. ## Common misunderstandings - Believing Django's URL resolver matches on the HTTP method: it does not; the same pattern always reaches the view. - Expecting a missing handler to raise `AttributeError` and a 500: the `getattr` default turns it into a 405. - Writing a `head()` method to make `HEAD` work: unnecessary unless it must differ from `get()`. - Assuming `trace` is served because it appears in the default list.
- A view sets http_method_names = ["get"]. What happens to HEAD and OPTIONS requests?Both return 405. `dispatch()` checks the allow-list before looking for a handler, so even though `setup()` aliases `head` to `get` and `View` defines `options()`, neither name is in the list. The `Allow` header on those 405s lists only `GET`. Use `["get", "head", "options"]` to make a view read-only without losing them.
- How would you accept a non-standard verb such as PURGE on one class-based view?Add `"purge"` to that view's `http_method_names` and define a `purge(self, request, *args, **kwargs)` method. `dispatch()` lowercases the incoming method, finds it in the list and calls the handler; without either half the request still ends in `http_method_not_allowed()`. The URLconf needs no change because the resolver ignores the method.
saying these in an interview costs you the question
- Saying a missing post() makes Django raise AttributeError and return 500
- Claiming the URL resolver returns 404 when no handler matches the method
- Believing HEAD needs its own head() method to work
- Thinking every verb in http_method_names is served, including trace
- Assuming defining a handler is enough without listing it in http_method_names