skip to content

How does Django's RedirectView build its target from url or pattern_name, and what happens to captured URL arguments and the query string?

level: middleimportance: should knowfreq 42%

answer

  1. string formatting vs reversing
  2. percent signs need doubling
  3. same args passed to reverse
  4. query string dropped by default
  5. nothing to redirect to means 410

basics

~20 s

RedirectView.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 lines
python
from 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

for a junior

Recall that RedirectView takes either a url string or a pattern_name and that permanent=False is the default.

for a middle

Explain url % kwargs versus reverse(pattern_name, args, kwargs), the query_string flag, and the 410 when no target exists.

for a senior

Anticipate migration breakage: extra captured kwargs causing NoReverseMatch, dropped campaign parameters, and overrides that bypass query-string handling.

for a principal

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