Overview
Every API request in DRF flows through a well-defined pipeline. Understanding this flow helps you debug issues and customize behavior at the right points.The Request Object
DRF wraps Django’sHttpRequest in its own Request class.
Why Wrap HttpRequest?
# From rest_framework/request.py:1-10
"""
The Request class is used as a wrapper around the standard request object.
The wrapped request then offers a richer API, in particular:
- content automatically parsed according to `Content-Type` header,
and available as `request.data`
- full support of PUT method, including support for file uploads
- form overloading of HTTP method, content type and content
"""
Creating the Request Wrapper
# From rest_framework/views.py:391-403
def initialize_request(self, request, *args, **kwargs):
"""
Returns the initial request object.
"""
parser_context = self.get_parser_context(request)
return Request(
request,
parsers=self.get_parsers(),
authenticators=self.get_authenticators(),
negotiator=self.get_content_negotiator(),
parser_context=parser_context
)
Request Attributes
request.data
Unified access to parsed request body:# Works for JSON, form data, multipart, etc.
def create(self, request):
title = request.data.get('title')
content = request.data.get('content')
# ...
Unlike Django’s
request.POST which only works with form data, request.data works with any content type that has a parser.request.query_params
Semantically correct name for GET parameters:# Instead of request.GET
search = request.query_params.get('search', '')
page = request.query_params.get('page', 1)
request.user and request.auth
Authentication information:if request.user.is_authenticated:
articles = Article.objects.filter(author=request.user)
else:
articles = Article.objects.filter(published=True)
# request.auth contains token, session, etc.
if request.auth:
# Token or other auth credentials
token = request.auth
Lazy Parsing
Request parsing happens on-demand:# From rest_framework/request.py:218-222
@property
def data(self):
if not _hasattr(self, '_full_data'):
with wrap_attributeerrors():
self._load_data_and_files()
return self._full_data
The request body is only parsed when you first access
request.data. This saves processing for requests that don’t need the body.Content Type Negotiation
DRF automatically selects the right parser:# From rest_framework/request.py:326-376
def _parse(self):
"""
Parse the request content, returning a two-tuple of (data, files)
"""
media_type = self.content_type
stream = self.stream
parser = self.negotiator.select_parser(self, self.parsers)
if not parser:
raise exceptions.UnsupportedMediaType(media_type)
try:
parsed = parser.parse(stream, media_type, self.parser_context)
except Exception:
# Fill in empty data on error
self._data = QueryDict('', encoding=self._request._encoding)
self._files = MultiValueDict()
self._full_data = self._data
raise
return (parsed.data, parsed.files)
The Initial Pipeline
Before your view handler runs, DRF executes several steps:1. Request Initialization
# In dispatch(), first wrap the request
request = self.initialize_request(request, *args, **kwargs)
self.request = request
self.headers = self.default_response_headers
2. Content Negotiation
# From rest_framework/views.py:405-422
def initial(self, request, *args, **kwargs):
"""Runs anything that needs to occur prior to calling the method handler."""
self.format_kwarg = self.get_format_suffix(**kwargs)
# Perform content negotiation
neg = self.perform_content_negotiation(request)
request.accepted_renderer, request.accepted_media_type = neg
# Determine the API version
version, scheme = self.determine_version(request, *args, **kwargs)
request.version, request.versioning_scheme = version, scheme
# Security checks
self.perform_authentication(request)
self.check_permissions(request)
self.check_throttles(request)
3. Authentication (Lazy)
# From rest_framework/views.py:322-330
def perform_authentication(self, request):
"""
Perform authentication on the incoming request.
Note that if you override this and simply 'pass', then authentication
will instead be performed lazily, the first time either
`request.user` or `request.auth` is accessed.
"""
request.user # Trigger authentication
How Authentication Works
# From rest_framework/request.py:378-395
def _authenticate(self):
"""
Attempt to authenticate the request using each authentication instance
in turn.
"""
for authenticator in self.authenticators:
try:
user_auth_tuple = authenticator.authenticate(self)
except exceptions.APIException:
self._not_authenticated()
raise
if user_auth_tuple is not None:
self._authenticator = authenticator
self.user, self.auth = user_auth_tuple
return
self._not_authenticated() # Set AnonymousUser
Authentication runs through each authenticator in order until one succeeds. If all fail, the user is set to
AnonymousUser.4. Permission Checks
# From rest_framework/views.py:332-343
def check_permissions(self, request):
"""
Check if the request should be permitted.
Raises an appropriate exception if the request is not permitted.
"""
for permission in self.get_permissions():
if not permission.has_permission(request, self):
self.permission_denied(
request,
message=getattr(permission, 'message', None),
code=getattr(permission, 'code', None)
)
5. Throttle Checks
# From rest_framework/views.py:358-377
def check_throttles(self, request):
"""
Check if request should be throttled.
Raises an appropriate exception if the request is throttled.
"""
throttle_durations = []
for throttle in self.get_throttles():
if not throttle.allow_request(request, self):
throttle_durations.append(throttle.wait())
if throttle_durations:
durations = [
duration for duration in throttle_durations
if duration is not None
]
duration = max(durations, default=None)
self.throttled(request, duration)
The View Handler
After the initial pipeline, your view handler executes:# From rest_framework/views.py:502-512
try:
self.initial(request, *args, **kwargs)
# Get the appropriate handler method
if request.method.lower() in self.http_method_names:
handler = getattr(self, request.method.lower(),
self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed
response = handler(request, *args, **kwargs)
Typical Handler Flow
def post(self, request, *args, **kwargs):
# 1. Get serializer with request data
serializer = self.get_serializer(data=request.data)
# 2. Validate
serializer.is_valid(raise_exception=True)
# 3. Save (calls create() or update())
self.perform_create(serializer)
# 4. Return response
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
The Response Object
DRF’s Response class is initialized with unrendered data:# From rest_framework/response.py:1-6
"""
The Response class in REST framework is similar to HTTPResponse, except that
it is initialized with unrendered data, instead of a pre-rendered string.
The appropriate renderer is called during Django's template response rendering.
"""
Creating Responses
# Simple response
return Response({'message': 'Hello, world!'})
# With status code
return Response(serializer.data, status=status.HTTP_201_CREATED)
# With headers
return Response(
serializer.data,
status=status.HTTP_201_CREATED,
headers={'Location': '/api/articles/123/'}
)
# Error response
return Response(
{'error': 'Not found'},
status=status.HTTP_404_NOT_FOUND
)
Response Initialization
# From rest_framework/response.py:20-47
def __init__(self, data=None, status=None,
template_name=None, headers=None,
exception=False, content_type=None):
"""
Alters the init arguments slightly.
For example, drop 'template_name', and instead use 'data'.
Setting 'renderer' and 'media_type' will typically be deferred,
For example being set automatically by the `APIView`.
"""
super().__init__(None, status=status)
if isinstance(data, Serializer):
msg = (
'You passed a Serializer instance as data, but '
'probably meant to pass serialized `.data` or '
'`.error`. representation.'
)
raise AssertionError(msg)
self.data = data
self.template_name = template_name
self.exception = exception
self.content_type = content_type
if headers:
for name, value in headers.items():
self[name] = value
Never pass a serializer instance to Response. Always pass
serializer.data or serializer.errors.Response Finalization
After the handler returns, the response is finalized:# From rest_framework/views.py:424-452
def finalize_response(self, request, response, *args, **kwargs):
"""
Returns the final response object.
"""
assert isinstance(response, HttpResponseBase), (
'Expected a `Response`, `HttpResponse` or `StreamingHttpResponse` '
'to be returned from the view, but received a `%s`'
% type(response)
)
if isinstance(response, Response):
if not getattr(request, 'accepted_renderer', None):
neg = self.perform_content_negotiation(request, force=True)
request.accepted_renderer, request.accepted_media_type = neg
response.accepted_renderer = request.accepted_renderer
response.accepted_media_type = request.accepted_media_type
response.renderer_context = self.get_renderer_context()
# Add vary headers
vary_headers = self.headers.pop('Vary', None)
if vary_headers is not None:
patch_vary_headers(response, cc_delim_re.split(vary_headers))
for key, value in self.headers.items():
response[key] = value
return response
Rendering
The response is rendered by Django’s TemplateResponse system:# From rest_framework/response.py:54-85
@property
def rendered_content(self):
renderer = getattr(self, 'accepted_renderer', None)
accepted_media_type = getattr(self, 'accepted_media_type', None)
context = getattr(self, 'renderer_context', None)
assert renderer, ".accepted_renderer not set on Response"
assert accepted_media_type, ".accepted_media_type not set on Response"
assert context is not None, ".renderer_context not set on Response"
context['response'] = self
media_type = renderer.media_type
charset = renderer.charset
content_type = self.content_type
if content_type is None and charset is not None:
content_type = f"{media_type}; charset={charset}"
elif content_type is None:
content_type = media_type
self['Content-Type'] = content_type
ret = renderer.render(self.data, accepted_media_type, context)
if isinstance(ret, str):
return ret.encode(charset)
return ret
How Renderers Work
class JSONRenderer:
media_type = 'application/json'
charset = 'utf-8'
def render(self, data, accepted_media_type=None, renderer_context=None):
if data is None:
return b''
return json.dumps(data).encode(self.charset)
Exception Handling
Exceptions are caught and converted to responses:# From rest_framework/views.py:454-478
def handle_exception(self, exc):
"""
Handle any exception that occurs, by returning an appropriate response,
or re-raising the error.
"""
if isinstance(exc, (exceptions.NotAuthenticated,
exceptions.AuthenticationFailed)):
# WWW-Authenticate header for 401 responses
auth_header = self.get_authenticate_header(self.request)
if auth_header:
exc.auth_header = auth_header
else:
exc.status_code = status.HTTP_403_FORBIDDEN
exception_handler = self.get_exception_handler()
context = self.get_exception_handler_context()
response = exception_handler(exc, context)
if response is None:
self.raise_uncaught_exception(exc)
response.exception = True
return response
The Default Exception Handler
# From rest_framework/views.py:72-102
def exception_handler(exc, context):
"""
Returns the response that should be used for any given exception.
By default we handle the REST framework `APIException`, and also
Django's built-in `Http404` and `PermissionDenied` exceptions.
"""
if isinstance(exc, Http404):
exc = exceptions.NotFound(*(exc.args))
elif isinstance(exc, PermissionDenied):
exc = exceptions.PermissionDenied(*(exc.args))
if isinstance(exc, exceptions.APIException):
headers = {}
if getattr(exc, 'auth_header', None):
headers['WWW-Authenticate'] = exc.auth_header
if getattr(exc, 'wait', None):
headers['Retry-After'] = '%d' % exc.wait
if isinstance(exc.detail, (list, dict)):
data = exc.detail
else:
data = {'detail': exc.detail}
set_rollback() # Rollback database on error
return Response(data, status=exc.status_code, headers=headers)
return None # Let Django handle it (500 error)
Custom Exception Handler
from rest_framework.views import exception_handler
def custom_exception_handler(exc, context):
# Call REST framework's default exception handler first
response = exception_handler(exc, context)
if response is not None:
# Add custom error code
response.data['error_code'] = getattr(exc, 'code', 'unknown')
# Add request info for debugging
if settings.DEBUG:
response.data['path'] = context['request'].path
response.data['method'] = context['request'].method
return response
# In settings.py
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'myapp.utils.custom_exception_handler'
}
Complete Request Flow Example
Let’s trace a complete POST request:# 1. Client sends request
POST /api/articles/
Content-Type: application/json
Authorization: Token abc123
{"title": "DRF Deep Dive", "content": "..."}
# 2. Django routes to ViewSet
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
# 3. APIView.dispatch() is called
def dispatch(self, request, *args, **kwargs):
# a. Wrap request
request = self.initialize_request(request)
# b. Run initial pipeline
self.initial(request)
# 4. initial() pipeline
def initial(self, request):
# a. Content negotiation
request.accepted_renderer = JSONRenderer()
request.accepted_media_type = 'application/json'
# b. Authentication (lazy)
self.perform_authentication(request)
# -> TokenAuthentication.authenticate(request)
# -> request.user = User.objects.get(token='abc123')
# c. Permission check
self.check_permissions(request)
# -> IsAuthenticated.has_permission(request, view)
# -> return request.user.is_authenticated
# d. Throttle check
self.check_throttles(request)
# 5. Handler method (from CreateModelMixin)
def create(self, request, *args, **kwargs):
# a. Get serializer with data
serializer = self.get_serializer(data=request.data)
# b. Validate
serializer.is_valid(raise_exception=True)
# -> serializer.to_internal_value(data)
# -> serializer.run_validators()
# -> serializer.validate()
# c. Save
self.perform_create(serializer)
# -> serializer.save(author=request.user)
# -> serializer.create(validated_data)
# -> Article.objects.create(**validated_data)
# d. Return response
headers = self.get_success_headers(serializer.data)
return Response(
serializer.data,
status=status.HTTP_201_CREATED,
headers=headers
)
# 6. finalize_response()
def finalize_response(self, request, response):
# a. Set renderer
response.accepted_renderer = request.accepted_renderer
response.accepted_media_type = request.accepted_media_type
# b. Set renderer context
response.renderer_context = {
'view': self,
'request': request,
}
# c. Add headers
response['Location'] = '/api/articles/123/'
return response
# 7. Render response
response.rendered_content
# -> JSONRenderer.render(response.data)
# -> json.dumps({'id': 123, 'title': 'DRF Deep Dive', ...})
# 8. Send to client
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/articles/123/
{"id": 123, "title": "DRF Deep Dive", "content": "...", "author": 1}
Customization Points
You can customize the request-response cycle at many points:1. Request Parsing
class MyView(APIView):
parser_classes = [JSONParser, FormParser, MultiPartParser]
2. Authentication
class MyView(APIView):
authentication_classes = [SessionAuthentication, TokenAuthentication]
def perform_authentication(self, request):
# Custom authentication logic
super().perform_authentication(request)
3. Permissions
class MyView(APIView):
permission_classes = [IsAuthenticated, IsOwner]
def check_permissions(self, request):
# Additional permission checks
super().check_permissions(request)
if not self.custom_check():
self.permission_denied(request)
4. Handler Execution
class MyViewSet(viewsets.ModelViewSet):
def create(self, request, *args, **kwargs):
# Pre-processing
data = self.preprocess_data(request.data)
# Standard create
response = super().create(request, *args, **kwargs)
# Post-processing
self.send_notification(response.data)
return response
5. Response Rendering
class MyView(APIView):
renderer_classes = [JSONRenderer, BrowsableAPIRenderer]
def finalize_response(self, request, response, *args, **kwargs):
# Custom headers or processing
response = super().finalize_response(request, response, *args, **kwargs)
response['X-Custom-Header'] = 'value'
return response
6. Exception Handling
class MyView(APIView):
def handle_exception(self, exc):
# Custom exception logic
if isinstance(exc, MyCustomException):
return Response({'error': str(exc)}, status=400)
return super().handle_exception(exc)
Performance Considerations
1. Lazy Authentication
Authentication doesn’t run untilrequest.user is accessed:
def get(self, request):
# No authentication yet
if 'skip_auth' in request.query_params:
return Response({'status': 'ok'})
# Authentication runs here
if request.user.is_authenticated:
return Response({'user': request.user.username})
2. Lazy Parsing
Request body isn’t parsed untilrequest.data is accessed:
def delete(self, request, pk):
# No parsing needed for DELETE
obj = self.get_object()
obj.delete()
return Response(status=204)
3. Database Optimization
class ArticleViewSet(viewsets.ModelViewSet):
def get_queryset(self):
# Optimize based on action
queryset = Article.objects.all()
if self.action == 'list':
queryset = queryset.only('id', 'title', 'created')
elif self.action == 'retrieve':
queryset = queryset.select_related('author').prefetch_related('tags')
return queryset
Summary
The DRF request-response cycle:- Request wrapping: Django HttpRequest → DRF Request
- Initial pipeline: Content negotiation, authentication, permissions, throttling
- Handler execution: Your view logic runs
- Response creation: Data → Response object
- Response finalization: Set renderer, add headers
- Rendering: Python dict → JSON/XML/etc
- Exception handling: Catches and formats errors at any point
Every step is customizable through method overrides or policy classes. Start with defaults and override only what you need.
