Framework Cookbooks

Status Code Handling in Django

A practical cookbook for returning correct HTTP status codes in Django, covering HttpResponse, JsonResponse, DRF exception handlers, middleware, and redirect patterns.

Django's HttpResponse and Status Codes

Every Django view returns an HttpResponse. The status parameter sets the HTTP status code. Django ships constants in django.http and shortcut classes for the most common codes.

from django.http import HttpResponse, Http404

def my_view(request):
    # 200 OK (default)
    return HttpResponse('Hello World')

def created_view(request):
    # 201 Created
    return HttpResponse('Resource created', status=201)

def no_content_view(request):
    # 204 No Content — body must be empty
    return HttpResponse(status=204)

Prefer named constants from http.HTTPStatus over raw integers to avoid typos:

from http import HTTPStatus
from django.http import HttpResponse

return HttpResponse(status=HTTPStatus.UNPROCESSABLE_ENTITY)  # 422

JsonResponse for API Errors

JsonResponse is a subclass of HttpResponse that serializes a dict to JSON and sets Content-Type: application/json. Always set status explicitly for error responses.

from django.http import JsonResponse

def api_view(request):
    if not request.user.is_authenticated:
        return JsonResponse(
            {'error': 'Authentication required', 'code': 'unauthenticated'},
            status=401,
        )

    resource = get_resource_or_none(request)
    if resource is None:
        return JsonResponse(
            {'error': 'Not found', 'code': 'not_found'},
            status=404,
        )

    return JsonResponse({'data': resource.to_dict()})

For validation errors, use 422 Unprocessable Entity rather than 400:

def create_user(request):
    form = UserForm(request.POST)
    if not form.is_valid():
        return JsonResponse(
            {'errors': form.errors},
            status=422,
        )

Custom Exception Handlers with DRF

Django REST Framework lets you define a global exception handler via EXCEPTION_HANDLER in REST_FRAMEWORK settings.

# settings.py
REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'myapp.exceptions.custom_exception_handler',
}

# myapp/exceptions.py
from rest_framework.views import exception_handler
from rest_framework.response import Response

def custom_exception_handler(exc, context):
    response = exception_handler(exc, context)

    if response is not None:
        # Wrap DRF errors in a consistent envelope
        response.data = {
            'error': {
                'status': response.status_code,
                'detail': response.data,
            }
        }

    return response

For domain-level exceptions, raise ValidationError or a custom exception that maps to a specific status code:

from rest_framework.exceptions import APIException

class PaymentRequired(APIException):
    status_code = 402
    default_detail = 'Payment is required to access this resource.'
    default_code = 'payment_required'

Middleware for Error Transformation

Process-wide error transformation belongs in middleware, not individual views. Override process_exception to intercept unhandled exceptions:

import logging
from django.http import JsonResponse

logger = logging.getLogger(__name__)

class APIErrorMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        return self.get_response(request)

    def process_exception(self, request, exception):
        if not request.path.startswith('/api/'):
            return None  # Let Django handle non-API exceptions

        logger.exception('Unhandled API error', exc_info=exception)
        return JsonResponse(
            {'error': 'Internal server error', 'code': 'server_error'},
            status=500,
        )

Redirect Responses (301, 302, 307, 308)

Django's redirect shortcuts choose between permanent and temporary for you:

from django.shortcuts import redirect
from django.http import (
    HttpResponsePermanentRedirect,  # 301
    HttpResponseRedirect,           # 302
)

# Temporary (302) — safe default; browser re-sends GET
return redirect('/new-path/')

# Permanent (301) — browser caches; hard to undo
return redirect('/new-path/', permanent=True)

# 307 Temporary — preserves HTTP method (POST stays POST)
from django.http import HttpResponse
response = HttpResponse(status=307)
response['Location'] = '/new-path/'
return response

# 308 Permanent — preserves method + permanent
response = HttpResponse(status=308)
response['Location'] = '/new-path/'
return response

When to use each:

  • 301 — URL permanently changed (SEO: transfers link equity)
  • 302 — Temporary redirect, POST-Redirect-GET pattern
  • 307 — Temporary, method preserved (API endpoint moved briefly)
  • 308 — Permanent, method preserved (API endpoint renamed)

File Responses (206 Partial Content)

Use FileResponse for streaming files. Django handles 206 Partial Content for byte-range requests automatically when you pass as_attachment=False:

from django.http import FileResponse
import os

def serve_video(request, path):
    file_path = os.path.join(settings.MEDIA_ROOT, path)
    response = FileResponse(
        open(file_path, 'rb'),
        content_type='video/mp4',
    )
    # FileResponse automatically supports Range requests → 206
    return response

Common Patterns and Anti-Patterns

Patterns to follow:

  • Always return 404 via get_object_or_404() instead of catching DoesNotExist manually
  • Return 405 Method Not Allowed by using class-based views or @require_POST
  • Use 200 only when the response body contains the resource
  • Return 201 with a Location header pointing to the new resource

Anti-patterns to avoid:

  • Returning 200 OK with {'success': false} in the body — use proper status codes
  • Using 400 Bad Request for validation errors — prefer 422
  • Swallowing all exceptions and returning 500 without logging
  • Returning HTML error pages for API endpoints (check Accept header)

Protocoles associés

Termes du glossaire associés

Plus dans Framework Cookbooks