🇫🇷 La version française est disponible sur Hashnode : Le Système Digestif de Django : anatomie d'une requête HTTP vers Django
Before we start, let review a few words we'll keep using
Socket: ([Ref 1])
A socket is an endpoint for communication between two machines over a network. A king of pipe through which data flows. When a browser sends a request, it opens a socket connection to our server. When the server responds, it writes bytes back through that same socket.
TCP, HTTP: ([Ref 2])
TCP (Transmission Control Protocol) is the transport layer. It defines standards for handling reliable delivery of data between machines. HTTP runs on top of TCP. HTTP is just text, formatted in a specific way. A browser sends an HTTP message through a TCP connection to our server.
WSGI (Web Server Gateway Interface): ([Ref 3])
WSGI is the standard interface between a Python web application and a web server. A WSGI server ( Gunicorn, uWSGI ect ) sits between the internet and Django in our case: it accepts (manage) the TCP connection, reads the raw HTTP bytes, and calls Django with a standardized Python dictionary ( called: environ ( [Ref 4] ) and holding every thing related the to incoming request: method, path, headers, body stream ). Django never touches TCP or the raw socket directly. That boundary is WSGI's job.
Middleware: ([Ref 5])
Middleware exists in every serious web framework. Express, NestJs, Laravel , Rails ect. In Django, middleware is a layer that sits between the WSGI server and the view ( who contains our code about how we want to treat the incoming request ). Middleware can read, modify, or stop a request entirely: stopping it and returning a response immediately, without passing the request any further, is called short-circuiting.
ORM: ([Ref 6])
ORM stands for Object-Relational Mapper. It's the layer that translates between Python objects and database tables. Rails calls it ActiveRecord, Laravel calls it Eloquent: in Django it's just "the ORM.".
Still here? Good 👌. So now that we have a clear view about these words, Let's trace a request. 🗺️
1. Before Django, Raw Bytes comming from a client
A browser decides to call our API. It opens a socket [Ref 1] connection to our server and sends something like this, which is a typical http text format:
GET /users/42/ HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbG...
Accept: application/json
...
...
...
That's it. Plain text. For now, no Python, no Django, no objects. A formatted string traveling through the socket [Ref 1], controlled by the TCP [Ref 2] standard.
The WSGI server [Ref 3] receives this on the socket, parses the HTTP text, and builds the environ dict [Ref 4]. Then it calls Django. That is where our story picks up.
2. The Birth of HttpRequest
The entry point of every Django request is WSGIHandler.__call__(), defined in django/core/handlers/wsgi.py.
This is the exact line where the request stops being a dictionary and becomes a Python object:
# django/core/handlers/wsgi.py (simplified)
class WSGIHandler(base.BaseHandler):
def __call__(self, environ, start_response):
request = self.request_class(environ) # ← born here
response = self.get_response(request)
# ...
self.request_class is HttpRequest class. Django takes the raw environ dict and wraps it into a rich, usable Python object.
Here is what that transformation looks like:
Notice request.META always contains the full environ dict. Nothing is lost. It is just made accessible through a proper Python interface.
Are we following? Good 👌. HttpRequest is alive. Now it has to travel.
3. The Middleware flow [Ref 5]
This is where most people have a fuzzy mental model, and it's worth clearing up.
Middlewares flow is not a pipeline where each step ( middleware ) independently processes the request and passes it along. It's a stack of nested wrappers. Each middleware wraps arounFork in thed everything that comes after it.
Here is what a middleware actually looks like:
class OurExampleMiddleware:
def __init__(self, get_response):
self.get_response = get_response # the next layer
def __call__(self, request):
# → this runs before the view
do_something_with(request)
response = self.get_response(request) # call the next layer.
# So if we have n other layer before the view,
# all them will be executed, the view too,
# before we continue to the next instruction of the current method
# ← The, this runs after the view, on the way back out
do_something_with(response)
return response
Middlewares are like, chained. So, self.get_response is the next middleware in the chain () or the view itself if we are at the last middleware). Calling it sends the request deeper. Not calling it and returning a response. If a middleware return a response that way, directly is a short-circuit and the view never runs.
Django processes MIDDLEWARE top to bottom on the way in, bottom to top on the way back. Order is not optional.
Here is what the built-in middleware injects as the request travels through:
In a sense, and compared to the human digestive system, This is the intestinal tract of the request. Each layer absorbs one thing: session here, user identity there etc. and passes the rest along. By the time the request exits, it's been fully enriched. Short-circuiting? That's your body rejecting something before it reaches the bloodstream.
By the time the request reaches our view ( officially the bloodstream 😅 ), request.session and request.user are already there, added by related middleware. Not magic just, middleware flow doing its job quietly.
4. URL Routing: The Crossroads
HttpRequest has cleared the middleware stack. Django now needs to figure out which view should handle it.
URLResolver walks urlpatterns (the list we define in urls.py in the core folder containing settings), comparing the request path against each pattern. The first match wins. Captured groups become kwargs.
# urls.py
path("users/<int:pk>/", UserDetailView.as_view()),
# request to /users/42/ → kwargs = {"pk": 42}
If nothing matches, Django returns a 404 before our view code ever runs. At this step, no view, no serializer, no database. Just a 404 out.
Still with us? We're almost at the part where our code actually runs.
5. Our Code Finally Runs 💻
The view receives two things: the HttpRequest object and the resolved kwargs. Everything that happened before this (socket, WSGI, environ, middleware, routing ) was Django's machinery. This part is ours.
class UserDetailView(RetrieveAPIView):
def get(self, request, pk):
# request.user is already set ( thank AuthenticationMiddleware, during the middleware flow )
# pk came from the URL thank URLResolver
user = User.objects.get(pk=pk)
return Response(UserSerializer(user).data)
s
User.objects.get(pk=pk): this is where the ORM [Ref 6] steps in. Django translates this Python call into a SQL query, hits the database, and returns a Python object. One line from us; a lot happening underneath. [I tried to read this code in the django module in the past, and it was not a simple logics. Thanks to django contributors.]
For those of us using Django REST Framework: APIView.dispatch() ( The base of viewsets ect. ) adds one more execution layer before our .get() or .post(), or .create() or custom action view runs: authentication classes, permission classes, throttle classes. We'll go deep on this in a dedicated post. For now, just know it's there.
6. The Return Journey
Our view returns an HttpResponse. This is where the middleware stack unwinds.
The same chain runs in reverse: bottom to top. Each middleware's post-view logic executes as get_response(request) returns back up the call stack.
Headers like Content-Security-Policy, Strict-Transport-Security, and Set-Cookie are added on this return trip. The middleware that sits at the top of MIDDLEWARE is the last to touch the response. That's the nesting in action.
7. Out the Other End
WSGIHandler takes the HttpResponse object returned and then converts it back to bytes, according to the HTTP protocol
The client receives HTTP text:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 87
Set-Cookie: sessionid=abc123; HttpOnly; Path=/
...
...
...
{"id": 42, "name": "..."}
Full circle. Bytes in, bytes out. Everything in between was Django. Digestion complete.
That's the full trip
Most of us live in section 5. The view, the serializer, the business logic. That's where the product lives: that's normal.
But the next time request.user is an AnonymousUser we didn't expect, or a response header is missing, or a 404 shows up before our code even has a chance to run, we'll know exactly which layer to look at.
The machinery is not magic. It's layers, each doing one job, in a specific order. Knowing this also helps us customize some behavior, if required by our business logic.
Which layer have we had to dig into when debugging? Drop it in the comments.



Top comments (0)