In Django, what do vary_on_headers() and vary_on_cookie() add to a response, and when must a view use them explicitly?
answer
- patch, do not replace
- case-insensitive merge
- some middleware adds Vary already
- raw request.COOKIES is not tracked
basics
~20 sThey add header names to the response's Vary header after the view runs, merging with any existing value. A view needs them when its output depends on a request header or cookie that no Django middleware already declares.
solid answer
~40 s`vary_on_headers("Accept", "User-Agent")` wraps a view and, after it returns, calls `patch_vary_headers()` to add those names to `Vary`; `vary_on_cookie` is `vary_on_headers("Cookie")`. The patch appends only names not already present, compared case-insensitively, keeps existing entries, and collapses the header to `*` if `*` is among them. Several Django components already patch `Vary`: `SessionMiddleware` adds `Cookie` when the session was accessed, `CsrfViewMiddleware` adds `Cookie` when it sets the CSRF cookie, `LocaleMiddleware` adds `Accept-Language`, and `GZipMiddleware` or `gzip_page` add `Accept-Encoding`. You must add it yourself when the view reads something those do not track, such as `request.headers["Accept"]` for HTML-or-JSON, a raw preference cookie from `request.COOKIES`, or `User-Agent`. Django's own cache framework uses `Vary` to build its keys.
code
python · 16 linesfrom django.http import JsonResponse
from django.shortcuts import get_object_or_404, render
from django.views.decorators.vary import vary_on_cookie, vary_on_headers
from .models import Product
@vary_on_headers("Accept")
@vary_on_cookie # reads a raw currency cookie, which no middleware tracks
def product_detail(request, slug):
product = get_object_or_404(Product, slug=slug)
currency = request.COOKIES.get("currency", "EUR")
price = product.price_in(currency)
if "application/json" in request.headers.get("Accept", ""):
return JsonResponse({"slug": slug, "price": str(price), "currency": currency})
return render(request, "catalogue/product_detail.html", {"product": product, "price": price})go deeper
Know that vary_on_headers and vary_on_cookie add names to the Vary header and live in django.views.decorators.vary.
Explain patch_vary_headers' merge rules and which Django middleware already add Cookie, Accept-Language or Accept-Encoding.
Audit views that branch on Accept, raw cookies or custom headers and add Vary so neither Django's cache nor an edge cache serves the wrong variant.
Balance correct Vary declarations against cache fragmentation, and decide which variants belong in URLs instead of headers.
## What the decorators do Both live in `django.views.decorators.vary`: - `vary_on_headers(*headers)` returns a decorator; after the view returns a response, it calls `django.utils.cache.patch_vary_headers(response, headers)`; - `vary_on_cookie` is simply `vary_on_headers("Cookie")`. They do not change the body, they do not read the request, and they do not cache anything. Their only job is to declare, on the response, which request headers the content depends on. What caches do with that declaration is an HTTP topic; the Django question is how the header gets onto the response. ## How patch_vary_headers merges `patch_vary_headers()` is the single function every Django component uses: 1. it splits any existing `Vary` value into a list, preserving order; 2. it appends each new name that is not already present, comparing **case-insensitively**, so `accept-language` and `Accept-Language` are not duplicated; 3. if `*` ends up in the list, it replaces the whole header with `*`; 4. otherwise it joins the list with `", "`. The order is preserved deliberately, because cache implementations may hash the list. ## Who already sets Vary for you | Component | Adds | When | |---|---|---| | `SessionMiddleware` | `Cookie` | the view accessed `request.session` (or the session cookie was deleted) | | `CsrfViewMiddleware` | `Cookie` | it set or refreshed the CSRF cookie on this response | | `LocaleMiddleware` | `Accept-Language` | unless the language came from an `i18n_patterns` URL prefix | | `GZipMiddleware` / `gzip_page` | `Accept-Encoding` | the response is long enough to consider compressing | | `UpdateCacheMiddleware` | `Authorization` | it is caching a response to a request that carried `Authorization`, unless the response is `public` | This is why most pages that use `request.user` get `Vary: Cookie` without any decorator: authentication reads the session. ## When a view must add it explicitly Add a decorator whenever the view branches on request data that none of the above tracks: - **content negotiation**: a product page that returns JSON when `Accept` asks for it and HTML otherwise needs `@vary_on_headers("Accept")`; - **raw cookies**: reading `request.COOKIES["currency"]` directly does not touch the session, so no middleware adds `Cookie`; use `@vary_on_cookie`; - **device variants**: a compact layout chosen from `User-Agent` needs `@vary_on_headers("User-Agent")`, knowing it fragments caches heavily; - **custom headers**: an `X-Region` header set by your edge that changes prices. Forgetting it is a correctness bug: a cache in front of Django, or Django's own cache framework, can serve the JSON variant to a browser or the euro prices to a dollar customer. ## Placement rules - Put vary decorators **above** `condition()`, because a 304 returned by `condition()` skips inner decorators and must still carry `Vary`. - On class-based views, wrap them with `method_decorator(..., name="dispatch")`. - They work on `async def` views since Django 5.0. ## Debugging a wrong-variant report When users report seeing someone else's currency or a JSON page in the browser: 1. Request the URL twice with different values of the suspect header or cookie and compare the responses' `Vary` header. 2. Check which of the components in the table above ran for that view; a view that reads `request.COOKIES` without the session is the classic gap. 3. Add the missing name with `vary_on_headers()` or `vary_on_cookie`, above any `condition()`. 4. Clear the affected cache entries, because responses stored without the right `Vary` stay wrong until they expire. 5. Add a test asserting the header, for example that `"Accept"` appears in `response["Vary"]`. ## Relationship with Django's cache framework Django's per-view and site caches read the response's `Vary` header when they build cache keys, so these decorators directly influence what Django's own cache stores separately. That interaction, and how to avoid over-fragmenting a cache, belongs to the caching topic; the decorator's contract is only to declare the dependency honestly.
- A view reads request.user and renders a greeting. Does it need vary_on_cookie?Usually not. Authentication reads `request.session`, which marks the session accessed, and `SessionMiddleware` then adds `Vary: Cookie` itself. It is the views that read `request.COOKIES` directly, without touching the session, that need the decorator.
- What does patch_vary_headers do if the response already has Vary: Accept-Encoding and you add Cookie and accept-encoding?It keeps `Accept-Encoding`, skips `accept-encoding` because names are compared case-insensitively, and appends `Cookie`, giving `Vary: Accept-Encoding, Cookie`. Only a `*` entry would collapse the header to `Vary: *`.
saying these in an interview costs you the question
- vary_on_headers replaces any Vary header already on the response
- vary_on_cookie caches the page separately per user by itself
- Reading request.COOKIES makes SessionMiddleware add Vary: Cookie
- Header names passed to vary_on_headers are case-sensitive
- Vary decorators can safely sit below condition()