How does Django's RedirectView build its target from url or pattern_name, and what happens to captured URL arguments and the query string?
answer
- string formatting vs reversing
- percent signs need doubling
- same args passed to reverse
- query string dropped by default
- nothing to redirect to means 410
basics
~20 sRedirectView.get_redirect_url() interpolates URL kwargs into url with %-formatting, or else reverses pattern_name with the same captured args and kwargs. The query string is dropped unless query_string=True, and with neither url nor pattern_name it returns 410 Gone.
solid answer
~40 s`RedirectView.get_redirect_url()` checks `url` first: it formats it with `url % kwargs`, so `"/articles/%(slug)s/"` picks up the captured slug, and literal percent signs must be written `%%`. Only if `url` is empty does it `reverse(pattern_name, args=args, kwargs=kwargs)`, passing everything the old pattern captured, which raises `NoReverseMatch` if the target pattern does not accept those exact arguments. With `query_string=False`, the default, the incoming query string is discarded; with `True` it is appended, joined with `&` if the target already has one. If the method returns `None`, as it does when neither attribute is set, the view answers **410 Gone**. RedirectView routes HEAD, POST, PUT, PATCH, DELETE and OPTIONS to the same `get()`, and you override `get_redirect_url()` when the target needs a lookup.
code
python · 15 linesfrom django.shortcuts import get_object_or_404
from django.urls import reverse
from django.views.generic import RedirectView
from news.models import Story
class LegacyStoryRedirectView(RedirectView):
permanent = True
def get_redirect_url(self, *args, **kwargs):
story = get_object_or_404(Story, legacy_id=kwargs["legacy_id"])
if story.retracted:
return None # RedirectView answers 410 Gone
return reverse("article-detail", kwargs={"slug": story.slug})go deeper
Recall that RedirectView takes either a url string or a pattern_name and that permanent=False is the default.
Explain url % kwargs versus reverse(pattern_name, args, kwargs), the query_string flag, and the 410 when no target exists.
Anticipate migration breakage: extra captured kwargs causing NoReverseMatch, dropped campaign parameters, and overrides that bypass query-string handling.
Plan a URL migration so redirects stay maintainable, choosing pattern_name for stable targets and a lookup view where old and new keys differ.
## The job: moving a legacy URL A news site used to publish stories at `/news/<year>/<slug>/` and now serves them at `/articles/<slug>/`. Old links live in search results, newsletters and other sites, so the old paths must keep working by redirecting. Django's **`RedirectView`** (in `django.views.generic`) does this declaratively. Its whole behaviour lives in one hook, **`get_redirect_url(*args, **kwargs)`**, which receives what the URL pattern captured. ## url: string formatting against captured kwargs When `url` is set, the target is `self.url % kwargs`: ```python from django.urls import path from django.views.generic import RedirectView urlpatterns = [ path( "news/<int:year>/<slug:slug>/", RedirectView.as_view(url="/articles/%(slug)s/", permanent=True), ), ] ``` - **Named placeholders** such as `%(slug)s` are filled from the captured kwargs; unused kwargs such as `year` are simply ignored. - The interpolation **always** runs, even when nothing was captured, so a literal `%` in the target (for example an already-encoded `%20`) must be written **`%%`**. - A placeholder with no matching kwarg raises `KeyError`, which surfaces as a server error. ## pattern_name: reversing with the same arguments When `url` is empty and `pattern_name` is set, the view calls `reverse(pattern_name, args=args, kwargs=kwargs)`. Every captured argument is passed on: - If the target pattern takes exactly the same kwargs, this is the cleanest option: no hard-coded path, and renaming the target URL does not break the redirect. - If the old pattern captured **more** than the new one accepts, `year` in our example, `reverse()` raises **`NoReverseMatch`** and the request fails with a 500. Either use `url`, or override `get_redirect_url()` and call `reverse()` with only what the target needs. - `url` wins if both are set: the code checks `url` first and only falls through to `pattern_name` when `url` is empty. ## What happens to the query string | `query_string` | Incoming `/news/2019/budget/?utm_source=mail` | Location header | |---|---|---| | `False` (default) | Query string discarded | `/articles/budget/` | | `True` | Appended with `?` | `/articles/budget/?utm_source=mail` | | `True`, target already has `?page=2` | Appended with `&` | `/articles/budget/?page=2&utm_source=mail` | Losing campaign parameters silently is a common surprise after a URL migration; set `query_string=True` when the parameters matter downstream. ## When there is nothing to redirect to If `get_redirect_url()` returns `None` — neither `url` nor `pattern_name` set, or an override that decides the content is gone — `get()` returns **`HttpResponseGone` (410)** and logs a warning to the `django.request` logger. That makes `RedirectView.as_view(url=None)` a one-line way to retire a section on purpose, telling crawlers the page was removed rather than never existing. ## Which methods it answers Unlike most generic views, `RedirectView` defines `head()`, `post()`, `put()`, `patch()`, `delete()` and `options()` as calls to `get()`. So a POST to a moved URL is redirected too; whether the client repeats the POST at the new address depends on the status code (see `preserve_request`, added in Django 6.1). `TRACE` is not handled and gets 405. ## Choosing between url and pattern_name | Aspect | `url` | `pattern_name` | |---|---|---| | Target written as | A path or absolute URL string | The name of a URL pattern | | Captured arguments | Only those named in placeholders are used | All are passed to `reverse()` | | Surplus captured kwargs | Ignored | `NoReverseMatch` | | External targets | Yes, any `http`/`https` URL | No, only this project's patterns | | Survives renaming the target path | No, the string must be edited | Yes, the name stays stable | A reasonable rule: use `pattern_name` when the old and new patterns capture the same arguments, `url` when the target is external or the argument sets differ, and an override when a lookup is needed. ## Custom targets Override `get_redirect_url()` when the target needs logic — for example, old URLs used a numeric id and the new ones use a slug. Look the story up with `get_object_or_404()`, then return `super().get_redirect_url(...)` with adjusted kwargs, or build the path with `reverse()` directly. Return `None` to send a 410. Two cautions for overrides: - Returning your own URL without calling `super()` bypasses the base method's query-string handling, so `query_string=True` has no effect unless you append `request.META["QUERY_STRING"]` yourself. - Never build the target from unvalidated query parameters; a redirect view that takes its destination from the request is an open redirect.
- Why does a get_redirect_url() override that returns reverse(...) directly lose the query string even with query_string=True?The query-string handling lives inside the base `get_redirect_url()`: it reads `QUERY_STRING` from `request.META` and appends it when `query_string` is true. An override that returns its own URL without calling `super()` skips that step. Either append `self.request.META["QUERY_STRING"]` yourself or compute kwargs and delegate to `super().get_redirect_url()` with `pattern_name` set.
- The redirect target in url contains an encoded space, %20. What goes wrong and how do you fix it?`get_redirect_url()` always runs `self.url % kwargs`, so `%20` is read as a format specifier and raises or corrupts the URL. Write it as `%%20`; Python's formatting turns `%%` back into a single `%`, and the Location header gets `%20`.
saying these in an interview costs you the question
- Believing RedirectView forwards the query string by default
- Thinking pattern_name reversing silently drops captured kwargs the target does not accept
- Assuming a RedirectView without url or pattern_name returns 404
- Writing a literal percent sign in url without doubling it
- Believing RedirectView only responds to GET requests