1.Validation errors and the 4xx status codes
A 4xx says the problem is on the client's side, and the specific code says what to do about it. **400** — the request is malformed or fails validation; fix it and retry. **401** — no valid credentials; authenticate and retry. **403** — credentials are fine, the action is refused; retrying will not help. **404** — no such resource, or you may not know it exists. **409** — the request is valid but conflicts with the current state, such as a duplicate or a stale version. **422** — well-formed and syntactically valid, but it fails a business rule (DRF returns 400 for this by default). **429** — you are rate limited; wait for `Retry-After` and try again. In DRF you rarely write these numbers: you raise the matching exception and the framework sets the code.
Key features
- DRF renders `ValidationError` as 400 with a per-field mapping; the exception is what you raise, the code is what the framework sets
- 401 requires a `WWW-Authenticate` header, which DRF takes from the first authentication class — without one it must answer 403 instead
- 403 confirms the resource exists; where that is sensitive, filter in `get_queryset()` so both cases return 404
- DRF ships no 409 exception — subclass `APIException` with `status_code = 409` for a duplicate, a stale version, or an order that has already shipped
- 429 must carry `Retry-After`; DRF's `Throttled` sets it from the throttle's `wait()`
- An empty collection is 200 with `[]`, never 404 — the request succeeded and the answer is "nothing matched"
Choosing a 4xx by what the client should do next
- The request failed, and it is the client's side
- Fix the request — the body or the URL is wrong
- 400 Bad Request — malformed JSON, wrong type, missing required field
- 422 Unprocessable — valid shape, fails a business rule — DRF uses 400 unless told otherwise
- 405 Method Not Allowed — the URL exists; this verb does not
- Fix who you are — identity, not input
- 401 Unauthorized — authenticate and retry — needs a WWW-Authenticate header
- 403 Forbidden — we know who you are; retrying will not help
- 404 instead of 403 — when existence itself is the secret — filter the queryset
- Wait, or change the world — the request is fine; the state is not
- 409 Conflict — duplicate, stale version, already-shipped order
- 429 Too Many Requests — always with Retry-After — the client waits and repeats verbatim
- 200 with [] — an empty result is not an error — do not reach for 404
Command
class Conflict(APIException):
status_code = 409
default_detail = "Conflicts with the current state of the resource."
default_code = "conflict"Choose the code by what the client should do next: 400/422 fix the input, 401 authenticate, 403 give up, 404 it is not there (or you may not know), 409 the state conflicts, 429 wait for `Retry-After`. In DRF you raise the exception and the framework sets the code — and it ships none for 409, so define one. 403 confirms existence, so return 404 for both cases when existence is the secret. An empty result is 200 with `[]`, never 404.