In Django REST Framework, what happens when a view raises NotFound, PermissionDenied or Throttled, and what body does the default exception handler return?
answer
- one base class with three attributes
- status code comes from the class
- a detail key unless list or dict
- Django's 404 and 403 are mapped
- anything else becomes a 500
basics
~20 sDRF catches APIException subclasses raised in a view and returns a Response with the class's status_code and a body of {"detail": message}. Django's Http404 and PermissionDenied are mapped to 404 and 403; other exceptions propagate as a 500.
solid answer
~30 sEvery DRF error class subclasses `APIException`, which carries `status_code`, `default_detail` and `default_code`. When one is raised anywhere in `APIView.dispatch()`, `handle_exception()` passes it to the `EXCEPTION_HANDLER`, by default `rest_framework.views.exception_handler`. That handler maps Django's `Http404` to `NotFound` and Django's `PermissionDenied` to DRF's, then returns a `Response` with the exception's status and `{"detail": ...}`, or the detail itself when it is a list or dict, as with `ValidationError`. It adds `Retry-After` for `Throttled` and `WWW-Authenticate` for 401s. Any other exception makes the handler return `None`, so it is re-raised and Django answers 500. For your own cases, subclass `APIException`, for example a 409 `JobClosed`.
code
python · 23 linesfrom rest_framework import status
from rest_framework.exceptions import APIException, NotFound
from rest_framework.response import Response
from rest_framework.views import APIView
from jobs.models import Job
class JobClosed(APIException):
status_code = status.HTTP_409_CONFLICT
default_detail = "This job no longer accepts applications."
default_code = "job_closed"
class ApplyView(APIView):
def post(self, request, job_id):
job = Job.objects.filter(pk=job_id).first()
if job is None:
raise NotFound("Job not found.")
if job.is_closed:
raise JobClosed()
job.applications.create(applicant=request.user)
return Response(status=status.HTTP_201_CREATED)go deeper
Know the common exception classes and their status codes, and that the default body is a detail key with the message.
Explain the default handler's steps: Http404 and Django PermissionDenied mapping, list or dict passthrough, extra headers, None for everything else.
Explain the 401-to-403 coercion and the rollback call, and design custom APIException subclasses for domain conflicts instead of generic 400s.
Set a catalogue of error codes the API promises to clients and make custom exception classes the only way to raise them.
## The APIException family In Django REST Framework (DRF), every error the framework knows how to answer is a subclass of `rest_framework.exceptions.APIException`. The base class has three class attributes: - `status_code`: the HTTP status, 500 on the base class; - `default_detail`: the message used when none is passed; - `default_code`: a short machine-readable code such as `'not_found'`. The detail is stored as `ErrorDetail` objects, which are strings that also carry a `.code`. | Exception | Status | `default_code` | |---|---|---| | `ParseError` | 400 | `parse_error` | | `ValidationError` | 400 | `invalid` | | `AuthenticationFailed` | 401 | `authentication_failed` | | `NotAuthenticated` | 401 | `not_authenticated` | | `PermissionDenied` | 403 | `permission_denied` | | `NotFound` | 404 | `not_found` | | `MethodNotAllowed` | 405 | `method_not_allowed` | | `NotAcceptable` | 406 | `not_acceptable` | | `UnsupportedMediaType` | 415 | `unsupported_media_type` | | `Throttled` | 429 | `throttled` | ## What the default handler does `APIView.dispatch()` wraps `initial()` (negotiation, versioning, authentication, permissions, throttling) and the handler method in one `try`. Any exception goes to `handle_exception()`, which calls the function named by the `EXCEPTION_HANDLER` setting, `rest_framework.views.exception_handler` by default. That function: 1. converts Django's `django.http.Http404` into `NotFound`, keeping its message, and Django's `django.core.exceptions.PermissionDenied` into DRF's `PermissionDenied`; 2. for any `APIException`, builds headers: `WWW-Authenticate` if the exception carries an auth challenge, `Retry-After` if it carries a `wait`; 3. uses the detail directly when it is a list or dict, otherwise wraps it as `{"detail": ...}`; 4. calls `set_rollback()`, which marks the transaction for rollback when the database uses `ATOMIC_REQUESTS`; 5. returns a `Response` with the exception's status code; 6. returns `None` for anything else, so the exception is re-raised and Django's normal 500 handling applies. So `raise NotFound()` gives `404 {"detail": "Not found."}`; a Django `get_object_or_404()` miss gives `404 {"detail": "No Job matches the given query."}`; `Throttled(wait=29.2)` gives 429, `Retry-After: 30` and a detail ending 'Expected available in 30 seconds.' ## 401 or 403? `permission_denied()` raises `NotAuthenticated` when the view has authentication classes but none of them authenticated the request. `handle_exception()` then asks the **first** authentication class for a `WWW-Authenticate` challenge. `BasicAuthentication` and `TokenAuthentication` provide one, so the answer is 401. `SessionAuthentication`, first in the default `DEFAULT_AUTHENTICATION_CLASSES`, does not, so DRF **coerces the status to 403**. That is why an anonymous request to a default-configured API often gets 403 rather than 401. ## Raising your own For domain errors, subclass `APIException` and set the three attributes: - a partner tries to apply to a closed job: `JobClosed` with `status_code = 409` and `default_code = 'job_closed'`; - a dependency is down: a 503 class with a retry-friendly message. Raise it from the view, a serializer's `save()` or a permission class; the handler turns it into a response like any built-in one. Passing `detail=` or `code=` overrides the defaults per raise. ## What is not handled - Plain Python exceptions, database errors and Django's own `ValidationError` from `django.core.exceptions`: the handler returns `None`, Django returns a 500. - Errors outside the DRF view: middleware, unmatched URLs and rendering after the view returned never reach `EXCEPTION_HANDLER`.
- Why does an anonymous request to a default-configured DRF API get 403 rather than 401?DRF raises `NotAuthenticated`, but `handle_exception()` only keeps 401 if the first authentication class supplies a `WWW-Authenticate` challenge. `SessionAuthentication`, first by default, supplies none, so the status is coerced to 403. Put `BasicAuthentication` or `TokenAuthentication` first to get 401 with a challenge.
- How do you override the message or code of a built-in exception for one raise?Pass them to the constructor: `raise PermissionDenied(detail="Only the posting company can edit this job.", code="not_owner")`. The status stays 403, the body carries the new message, and `exc.get_codes()` returns `'not_owner'` for handlers that expose codes.
saying these in an interview costs you the question
- Every DRF error body has a detail key, validation errors included
- Raising Django's Http404 in a DRF view renders Django's HTML 404 page
- Any exception raised in a DRF view becomes a 400 response
- NotAuthenticated always produces a 401 status
- DRF adds Retry-After only when a custom handler sets it