In Django 6.1, which status codes can RedirectView send, and how do permanent and preserve_request choose between them?
answer
- two booleans, four codes
- temporary is the default
- keep the method and body
- new attribute in 6.1
basics
~10 sRedirectView sends 302 by default, 301 with permanent=True, and, since Django 6.1, 307 or 308 when preserve_request=True, which tells the client to repeat the same method and body at the new URL.
solid answer
~40 sTwo booleans pick the status. `permanent` (default `False`) chooses between `HttpResponseRedirect` (302) and `HttpResponsePermanentRedirect` (301). `preserve_request` (default `False`, new on `RedirectView` in Django 6.1) switches those to 307 and 308, the codes that require the client to resend the same method and body instead of turning a POST into a GET. Use the default 302 while a move is provisional, 301 once a page has moved for good, and 307 or 308 when the old URL receives POSTs or other non-GET requests, such as a form action or a webhook endpoint that moved. On Django 5.2 the response classes already accept `preserve_request`, but `RedirectView` does not, so `as_view(preserve_request=True)` raises `TypeError` there; override `get()` or return the response yourself.
code
python · 15 linesfrom django.urls import path
from django.views.generic import RedirectView
urlpatterns = [
# Page moved for good: 301
path(
"news/<int:year>/<slug:slug>/",
RedirectView.as_view(url="/articles/%(slug)s/", permanent=True),
),
# Form endpoint moved for now, keep POST and body: 307 (Django 6.1)
path(
"newsletter/subscribe/",
RedirectView.as_view(pattern_name="newsletter-signup", preserve_request=True),
),
]go deeper
Recall that RedirectView sends a temporary 302 by default and a 301 when permanent=True.
Explain the two-by-two table: permanent picks 301 or 302, preserve_request upgrades them to 308 or 307 so the method and body survive.
Match codes to the migration: 302 while provisional, 301 when final, 307 or 308 for moved POST endpoints, and know the 6.1-only attribute.
Set a redirect policy for URL migrations that accounts for client caching of permanent redirects and for non-browser callers posting to old endpoints.
## Two switches, four status codes Django's `RedirectView` decides the status code from two class attributes, both `False` by default: | `permanent` | `preserve_request` | Response class | Status | |---|---|---|---| | `False` | `False` | `HttpResponseRedirect` | **302** Found | | `True` | `False` | `HttpResponsePermanentRedirect` | **301** Moved Permanently | | `False` | `True` | `HttpResponseRedirect(..., preserve_request=True)` | **307** Temporary Redirect | | `True` | `True` | `HttpResponsePermanentRedirect(..., preserve_request=True)` | **308** Permanent Redirect | `RedirectView.get()` computes the URL, then constructs one of the two response classes, passing `preserve_request=self.preserve_request`. Each response class has a normal status code and a "preserve request" status code it swaps in when the flag is true. ## What each switch means - **`permanent`** tells clients and crawlers whether the move is final. The default has been `False` (temporary) since Django 1.9; before that it was `True`, which is why older tutorials describe `RedirectView` as permanent by default. - **`preserve_request`** tells the client to reuse the **same HTTP method and body** at the new location. With 301 and 302, clients may, and browsers in practice do, turn a redirected POST into a GET and drop the body. With 307 and 308 they must not. ## Version context - **Django 5.2** added the `preserve_request` argument to `HttpResponseRedirect`, `HttpResponsePermanentRedirect` and the `redirect()` shortcut. - **Django 6.1** added the `RedirectView.preserve_request` attribute, so the class-based view can do the same declaratively. On a 5.2 LTS project, `RedirectView.as_view(preserve_request=True)` fails when the URLconf loads with `TypeError`, because `as_view()` only accepts keywords that are already class attributes. Setting the attribute in a subclass on 5.2 is silently ignored, since 5.2's `get()` never reads it. Override `get()` and build the response with `preserve_request=True` instead. ## Choosing the code for a legacy URL Take a news site that moved `/news/<year>/<slug>/` to `/articles/<slug>/` and also moved its newsletter sign-up form's endpoint. 1. **During the migration**, keep `permanent=False`. A 302 is easy to change if the new URL scheme is revised. 2. **Once the new URLs are final**, switch to `permanent=True` so search engines and clients treat the new address as canonical. Clients may cache a 301 for a long time, so a wrong permanent redirect is hard to take back. 3. **For endpoints that receive POSTs** — the moved sign-up form, an incoming webhook, an API route — set `preserve_request=True`. With 302 the browser would re-issue the submission as a GET, and the form data would be lost. 4. **Leave `preserve_request` off for ordinary page moves.** For GET requests, 302 and 307 behave the same for the client, and the familiar codes are what monitoring and crawlers expect. ## How the response classes do it The mechanism is small enough to read in the source: - `HttpResponseRedirect` has `status_code = 302` and `status_code_preserve_request = 307`. - `HttpResponsePermanentRedirect` has `status_code = 301` and `status_code_preserve_request = 308`. - Their shared base constructor sets the `Location` header and, when `preserve_request` is true, replaces the status code with the preserving one. So `RedirectView` adds no status logic of its own beyond choosing the class and passing the flag through. ## Checking it in a test With Django's test `Client`, assert the code explicitly rather than only following the redirect: - `response = self.client.post("/newsletter/subscribe/", data)` should give `response.status_code == 307` when `preserve_request=True`. - `response.headers["Location"]` should be the new URL. - `assertRedirects()` checks the status and target together; pass `status_code=307` or `308` because its default expects 302. ## Common mistakes - Assuming `RedirectView` is permanent by default. - Redirecting a moved POST endpoint with 301 or 302 and wondering why the target receives GET requests with no body. - Using `permanent=True` on a URL scheme that is still changing. - Copying a Django 6.1 example with `preserve_request` into a 5.2 project.
- Why is permanent=True risky to switch on too early during a URL migration?A 301 tells clients the old address is gone for good, and browsers and crawlers may remember it for a long time. If the new scheme then changes again, clients keep jumping straight to the stale target without asking the server. Keep the default 302 until the new URLs are final, then switch to 301, or to 308 for endpoints that receive POSTs.
- How do you get a 307 from a RedirectView on Django 5.2 LTS?Subclass `RedirectView` and override `get()`: compute `url = self.get_redirect_url(*args, **kwargs)` and return `HttpResponseRedirect(url, preserve_request=True)`, or `HttpResponseGone()` when the URL is `None`. The response classes accept `preserve_request` since 5.2; only the view attribute is missing until 6.1.
saying these in an interview costs you the question
- Believing RedirectView sends a permanent 301 unless told otherwise
- Thinking preserve_request keeps the query string rather than the method and body
- Assuming a 302 redirect always makes the browser repeat a POST
- Expecting RedirectView.as_view(preserve_request=True) to work on Django 5.2
- Saying permanent and preserve_request are mutually exclusive