skip to content

Listing & Wire Contract

Pagination classes, filter backends, parsers and renderers, versioning schemes and the exception handler shape what an API accepts and returns. Interviewers probe cursor pagination and error bodies.

part ofDjango REST Frameworkoverview, primer and where to startread it →
on this pageshow

explore

questions

17

In Django REST Framework, what happens when a view raises NotFound, PermissionDenied or Throttled, and what body does the default exception handler return?

level: juniorimportance: must knowfreq 60%

answer

  1. one base class with three attributes
  2. status code comes from the class
  3. a detail key unless list or dict
  4. Django's 404 and 403 are mapped
  5. anything else becomes a 500

basics

~20 s

DRF 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 s

Every 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 lines
python
from 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

for a junior

Know the common exception classes and their status codes, and that the default body is a detail key with the message.

for a middle

Explain the default handler's steps: Http404 and Django PermissionDenied mapping, list or dict passthrough, extra headers, None for everything else.

for a senior

Explain the 401-to-403 coercion and the rollback call, and design custom APIException subclasses for domain conflicts instead of generic 400s.

for a principal

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
open as a page

In Django REST Framework, how do filter_backends let clients filter, search and order a list endpoint such as GET /jobs/?

level: juniorimportance: must knowfreq 62%

basics

~10 s

A DRF generic view passes its queryset through each class in filter_backends: DjangoFilterBackend (django-filter) handles field filters like ?remote=true, SearchFilter handles ?search= over search_fields, and OrderingFilter handles ?ordering= limited to ordering_fields.

open as a page

In Django REST Framework, how do you enable pagination for list endpoints, and what does PageNumberPagination put in the response?

level: juniorimportance: must knowfreq 58%

basics

~10 s

DRF paginates nothing by default: set DEFAULT_PAGINATION_CLASS and PAGE_SIZE, or pagination_class on a view. PageNumberPagination reads ?page=N and returns count, next, previous and results; a page beyond the end is a 404.

open as a page

How would you write a custom EXCEPTION_HANDLER in Django REST Framework so a partner API returns one error envelope, and what will it still miss?

level: middleimportance: must knowfreq 50%

basics

~20 s

Write a function (exc, context) that calls rest_framework.views.exception_handler, reshapes response.data into your envelope, and set it as EXCEPTION_HANDLER. It never sees responses a view returns itself, errors outside DRF views, or unhandled exceptions it returns None for.

open as a page

In Django REST Framework, why should a view using OrderingFilter declare ordering_fields explicitly, and what happens if you leave it unset or use '__all__'?

level: middleimportance: must knowfreq 50%

basics

~10 s

OrderingFilter's ordering_fields is the whitelist for ?ordering=. Unset, it allows every readable serializer field; 'all' allows every concrete model field and annotation. An explicit list stops clients sorting by hidden or unindexed columns.

open as a page

In Django REST Framework, why does CursorPagination need a stable, unique ordering, and how would you configure it for an infinite-scroll activity feed?

level: middleimportance: must knowfreq 50%

basics

~20 s

CursorPagination encodes the value of the first ordering field and fetches rows beyond it, so that field must be unchanging, indexed, non-null and nearly unique. For a feed, order by '-created' with a unique tiebreaker.

open as a page

In Django REST Framework, what JSON shape does ValidationError produce when raised with a string, a dict, from serializer errors and from a many=True serializer?

level: middleimportance: should knowfreq 44%

basics

~20 s

DRF's ValidationError never uses the detail wrapper: a string becomes a JSON list, a dict passes through, serializer errors map fields to message lists, and since 3.18 many=True errors are a dict keyed by the index of each invalid item.

open as a page

In Django REST Framework generic views, when should filtering live in get_queryset() rather than in a filter backend, and how do the two combine?

level: middleimportance: should knowfreq 47%

basics

~10 s

Put rules that always apply, or come from the URL, in get_queryset(); put optional, client-driven narrowing in filter backends. DRF runs filter_queryset(get_queryset()), so backends narrow the base set on list and every get_object() action.

open as a page

In Django REST Framework's SearchFilter, what do the ^, =, @ and $ prefixes on search_fields do, and how are several search terms combined?

level: middleimportance: should knowfreq 42%

basics

~10 s

SearchFilter prefixes choose the lookup: ^ istartswith, = iexact, @ full-text search (PostgreSQL only), $ iregex, none icontains. Each search term must match at least one of the search_fields, and all terms must match.

open as a page

How do you add a CSV output format to a Django REST Framework endpoint, and what must a custom renderer handle besides successful list data?

level: middleimportance: should knowfreq 33%

basics

~10 s

Subclass BaseRenderer with media_type 'text/csv' and format 'csv', return bytes from render(), and add it to renderer_classes alongside JSONRenderer. It must also cope with paginated envelopes, single objects and error dicts.

open as a page

In Django REST Framework, why does one endpoint return the Browsable API to a browser but JSON to curl, and how does DefaultContentNegotiation decide?

level: middleimportance: should knowfreq 45%

basics

~20 s

DRF matches the Accept header against the view's renderers: browsers ask for text/html, which BrowsableAPIRenderer serves, while curl sends /, which the first renderer, JSONRenderer by default, satisfies. q-values are ignored; ties go to renderer order.

open as a page

In Django REST Framework, how do URLPathVersioning, NamespaceVersioning and AcceptHeaderVersioning set request.version, and how do you serve a different serializer for v2?

level: middleimportance: should knowfreq 40%

basics

~10 s

With a versioning class set, DRF fills request.version before the handler: URLPathVersioning from the version URL kwarg, NamespaceVersioning from the URL namespace, AcceptHeaderVersioning from Accept's version parameter. Branch on it in get_serializer_class().

open as a page

In Django REST Framework, how do you change the paginated response envelope with get_paginated_response(), and how do you paginate inside a plain APIView?

level: middleimportance: should knowfreq 36%

basics

~10 s

Subclass a pagination class and override get_paginated_response(data) to return any Response, such as bare results with Link headers, plus get_paginated_response_schema() for OpenAPI. In an APIView, call paginate_queryset(), serialise the page, then call get_paginated_response().

open as a page

A partner sees 500s from a DRF endpoint when full_clean() fails in perform_create() or a PROTECT foreign key blocks a delete; why no 4xx, and how do you fix it?

level: seniorimportance: should knowfreq 34%

basics

~10 s

DRF's default handler only answers APIException, Http404 and Django's PermissionDenied. Django's ValidationError raised outside serializer validation and ProtectedError escape as 500s. Raise DRF exceptions at the source or map them in a custom handler.

open as a page

A DRF job-listings endpoint over millions of rows passes ?sort= straight to order_by() and uses SearchFilter with '$description'; what breaks in production and how do you harden it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Raw order_by() lets clients sort by any related or hidden field, by '?' for random order, or crash with FieldError (a 500); $ lets them run costly regexes. Use OrderingFilter with indexed ordering_fields, a unique tiebreaker, and non-regex search.

open as a page

A DRF list endpoint using LimitOffsetPagination on a very large table is slow and answers ?limit=1000000 — what causes both problems, and how do you harden it?

level: seniorimportance: should knowfreq 40%

basics

~10 s

LimitOffsetPagination's max_limit defaults to None, so any limit is honoured, and every page runs queryset.count(). Set max_limit, override get_count() or switch to CursorPagination for deep or unbounded lists.

open as a page