Grokking Django
Vote

0% completed

​

Understanding the Request-Response Cycle

The Cycle in Order

The Request Object

Middleware on the Way In

Middleware on the Way Out

The Response Object

Status Codes

When Something Goes Wrong

Request /articles without the final slash, and Django does not answer with "Page not found". It answers with a redirect to /articles/, even though no view in the blog contains any code for that. The redirect comes from a layer of code that wraps every view, and it is one of several things that happen to a request before your view runs and to the response after your view returns.

The whole journey is called the request-response cycle. The previous lesson followed a request from the browser to the view. This lesson covers the complete cycle around the view: the request object the view receives, the middleware the request and the response pass through, the response objects a view can return, and what Django sends back when something goes wrong.

The Cycle in Order

For every request, Django performs the same sequence of steps:

  1. The web server passes the raw request to Django, and Django builds an HttpRequest object from it.
  2. The request passes through each middleware layer, from the first entry in MIDDLEWARE to the last.
  3. The URL resolver finds the view, and Django calls it with the request.
  4. The view returns an HttpResponse object.
  5. The response passes back through the middleware layers in reverse order, from the last entry to the first.
  6. Django hands the response to the web server, which sends it to the browser.
The request passes down through seven middleware layers to the view, and the response passes back up through them in reverse order.
The request passes down through seven middleware layers to the view, and the response passes back up through them in reverse order.

In the diagram, notice that the request and the response cross the same seven layers, but in opposite directions. A layer near the top of the list sees the request first and the response last.

The Request Object

The request argument that every view receives is an HttpRequest object. It holds everything the browser sent. To see its most useful attributes, add a small view to the blog that prints them:

# blog/views.py from django.http import HttpResponse def request_info(request): lines = [ f"method: {request.method}", f"path: {request.path}", f"GET: {request.GET}", f"q: {request.GET.get('q', '')}", f"user agent: {request.headers.get('User-Agent', 'unknown')}", f"user: {request.user}", f"authenticated: {request.user.is_authenticated}", ] return HttpResponse("\n".join(lines), content_type="text/plain")

Add path('info/', views.request_info), to urlpatterns in blog/urls.py, then request /articles/info/?q=django from a browser. The user agent line is shortened here, because a real browser sends a much longer value:

method: GET path: /articles/info/ GET: <QueryDict: {'q': ['django']}> q: django user agent: Mozilla/5.0 user: AnonymousUser authenticated: False

Each line comes from one attribute:

  • request.method is the HTTP method, such as GET for reading a page or POST for submitting a form.
  • request.path is the path without the domain or the query string.
  • request.GET holds the query string parameters, and request.POST holds submitted form fields. Both are QueryDict objects, a dictionary that can hold several values for one key. Use .get() with a default, because a missing key in request.GET['q'] raises an error.
  • request.headers holds the HTTP headers, and you can look them up in any letter case. request.COOKIES holds the cookies as a dictionary.
  • request.user is the signed-in user, or an AnonymousUser object when nobody is signed in. request.session holds data Django keeps for this visitor between requests.

The browser never sent a user or a session. Those two attributes were added by middleware on the way in, which is the subject of the next section.

Middleware on the Way In

Middleware is a layer of code that runs for every request and every response, around all views. Each layer can read or change the request, stop it and answer immediately, or let it continue to the next layer. A new project lists seven layers in its settings:

# mysite/settings.py MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ]

On the way in, the request meets these layers from top to bottom. SessionMiddleware reads the session cookie and attaches request.session. AuthenticationMiddleware then uses that session to find the signed-in user and attaches request.user. This is why the order of the list matters: authentication needs the session, so SessionMiddleware must come before AuthenticationMiddleware.

A middleware layer can also refuse a request, and then the view never runs. CsrfViewMiddleware checks every POST request for the secret CSRF token that Django puts into its own forms. A POST to /articles/info/ from a page that has no token is stopped before it reaches request_info, and the development server logs the reason:

Forbidden (CSRF cookie not set.): /articles/info/

The browser receives a response with status 403 Forbidden. The correct fix is to include the token in your forms, which Chapter 5 shows, not to switch the check off.

Middleware on the Way Out

After the view returns a response, the response travels back through the same layers from bottom to top. Each layer can add headers to it, change it, or replace it. Look at the headers of the article list page:

Content-Type: text/html; charset=utf-8 X-Frame-Options: DENY Content-Length: 320 X-Content-Type-Options: nosniff Referrer-Policy: same-origin Cross-Origin-Opener-Policy: same-origin

The view set only the content. XFrameOptionsMiddleware, the last layer, added X-Frame-Options: DENY, which tells browsers not to show the page inside a frame on another site. That header is the clickjacking protection from the first lesson of this chapter. SecurityMiddleware, the first layer, added the last three security headers.

The missing slash from the start of this lesson is handled on the way out as well. When the URL resolver finds no pattern for /articles, Django produces a 404 response. On its way back up, the 404 reaches CommonMiddleware. That layer sees that /articles/ would have matched, and it replaces the 404 with a redirect, because the APPEND_SLASH setting is True by default. These are the headers the browser receives:

Content-Type: text/html; charset=utf-8 Location: /articles/ Content-Length: 0 X-Content-Type-Options: nosniff Referrer-Policy: same-origin Cross-Origin-Opener-Policy: same-origin

The status is 301 Moved Permanently. The X-Frame-Options header is missing because XFrameOptionsMiddleware added it to the 404 response, which CommonMiddleware then threw away. The security headers are present because SecurityMiddleware runs after CommonMiddleware on the way out. This one response shows the reverse order clearly. Chapter 8 shows how to write middleware of your own.

The Response Object

A view must return an HttpResponse object or an object of one of its subclasses. A plain HttpResponse has status 200 and the content type text/html; charset=utf-8 unless you choose otherwise. You can check this in the shell that python manage.py shell opens:

from django.http import HttpResponse response = HttpResponse("Created", status=201) print(response.status_code, response['Content-Type'])
201 text/html; charset=utf-8

The render() shortcut from earlier lessons also returns an HttpResponse, with the rendered template as its content. Django provides other response classes and shortcuts for common cases:

Code in the viewClass returnedStatus
HttpResponse("text")HttpResponse200
render(request, 'blog/welcome.html')HttpResponse200
JsonResponse({'id': 1})JsonResponse200
redirect('/articles/')HttpResponseRedirect302
redirect('/articles/', permanent=True)HttpResponsePermanentRedirect301
HttpResponseNotFound("<h1>No such article</h1>")HttpResponseNotFound404

JsonResponse turns a dictionary into JSON and sets the content type to application/json. It is used by views that serve data to JavaScript code or to mobile apps instead of HTML pages. Add this view and the pattern path('<int:pk>/json/', views.article_json), to the blog:

# blog/views.py from django.http import JsonResponse from django.shortcuts import get_object_or_404 from .models import Article def article_json(request, pk): article = get_object_or_404(Article, pk=pk) return JsonResponse({'id': article.pk, 'title': article.title})

A request to /articles/1/json/ returns this body:

{"id": 1, "title": "Hello Django"}

redirect() sends the browser to another address, and the browser then makes a second request to it. A temporary redirect (302) is the default, which tells the browser to keep using the old address in future. A permanent redirect (301) tells browsers and search engines that the old address has moved permanently, and browsers may remember it. Use 301 only when you are sure the move is final. For example, this view always sends readers to the article list:

# blog/views.py from django.shortcuts import redirect def latest(request): return redirect('/articles/')

Status Codes

Every response carries a three-digit status code that tells the browser what happened. The first digit gives the class: 2 means success, 3 means redirection, 4 means the request was wrong, and 5 means the server failed. These are the codes you meet most often in this course:

CodeNameWhen Django sends it
200OKA view returned a page or data normally
201CreatedAn API created a new record
301Moved PermanentlyA permanent redirect, such as the missing slash
302FoundA temporary redirect, the default of redirect()
403ForbiddenA request was refused, such as a failed CSRF check
404Not FoundNo URL pattern matched, or a view raised Http404
405Method Not AllowedThe view does not accept this method
500Internal Server ErrorCode raised an exception that nothing handled

When Something Goes Wrong

The article_detail view from the previous lesson used Article.objects.get(pk=pk). When no article has that id, get() raises Article.DoesNotExist. Nothing in the view catches it, so Django treats it as a server error, and the reader sees status 500 for what is really a missing page.

The fix is to raise Http404 instead. The get_object_or_404() shortcut does that for you:

# blog/views.py from django.http import HttpResponse from django.shortcuts import get_object_or_404 from .models import Article def article_detail(request, pk): article = get_object_or_404(Article, pk=pk) return HttpResponse(article.title)

Now /articles/999/ returns status 404. Django catches the Http404 exception close to the view and turns it into a 404 response. Because the exception has become an ordinary response, it still travels back through the middleware layers, and the 404 page carries the same X-Frame-Options header as any other page.

What the reader sees depends on the DEBUG setting. While DEBUG is True, Django shows a detailed page with the message "No Article matches the given query.", the view that raised the error, and the URL patterns it tried. With DEBUG set to False, as it must be on a public site, Django shows a short page instead:

<h1>Not Found</h1><p>The requested resource was not found on this server.</p>

Any other exception that escapes a view becomes a 500 response in the same way. To see it, add a view that divides by zero, together with the pattern path('broken/', views.broken),, and request /articles/broken/:

# blog/views.py def broken(request): return 1 / 0

With DEBUG = True, the browser shows the full traceback, the local variables, and the settings, and the development server prints the traceback in the terminal:

Internal Server Error: /articles/broken/ Traceback (most recent call last): ... ZeroDivisionError: division by zero

With DEBUG = False, the reader sees only a short error page:

<h1>Server Error (500)</h1><p></p>

The detailed pages are helpful while you develop, but they show your code and settings to anyone who triggers an error, which is why a production site must run with DEBUG = False. You can replace the two short pages with your own design by adding templates named 404.html and 500.html, which Chapter 9 covers together with logging.

Reading Progress

0%


Vote for new content

On This Page

The Cycle in Order

The Request Object

Middleware on the Way In

Middleware on the Way Out

The Response Object

Status Codes

When Something Goes Wrong