skip to content

In Django, with APPEND_SLASH at its default, what happens to a request for /events/launch-party when only events/<slug:slug>/ is routed?

level: middleimportance: should knowfreq 47%

answer

  1. the default is on
  2. only when the slashed path resolves
  3. a permanent redirect
  4. appends, never strips
  5. form posts and DEBUG

basics

~10 s

APPEND_SLASH defaults to True, so CommonMiddleware replaces the 404 with a 301 redirect to /events/launch-party/, because the slashless path matches nothing and the slashed one resolves. Without CommonMiddleware the setting does nothing.

solid answer

~40 s

`APPEND_SLASH` is `True` by default and is read by `CommonMiddleware`, which `startproject` puts in `MIDDLEWARE`. When a path without a trailing slash resolves to nothing but the same path with a slash does, Django answers with a 301 to the slashed URL, keeping the query string. It only appends: a route written without a slash never matches a request that has one. For URLconf design that means ending routes with `/` consistently, never starting them with `/`, and linking through reversed URLs. Unsafe methods are the trap: with `DEBUG=True` a slashless POST, PUT, PATCH or DELETE raises `RuntimeError`, because a redirect cannot preserve the body; in production the 301 is sent and submitted data may be lost. `no_append_slash` exempts a view, and a catch-all that matches the slashless path suppresses the redirect.

go deeper

for a junior

Recall that APPEND_SLASH defaults to True and turns a slashless 404 into a redirect to the slashed URL when that URL exists.

for a middle

Explain the conditions for the redirect, the 301 status, the DEBUG RuntimeError for unsafe methods, and why routes should end with a slash and never start with one.

for a senior

Diagnose lost form posts and API clients hitting redirects, and spot catch-all routes or mixed slash conventions that make the redirect fire or vanish unexpectedly.

for a principal

Pick one trailing-slash convention per project, slashed pages or slashless APIs with APPEND_SLASH off, and enforce it so URLs stay canonical.

## The setting and what reads it `APPEND_SLASH` is a Django setting whose default in `global_settings.py` is `True`. It does nothing on its own: `django.middleware.common.CommonMiddleware`, which the `startproject` template includes in `MIDDLEWARE`, reads it. With both in place, a request whose path does not end in `/` and resolves to no pattern gets a second chance: if the same path with a slash appended resolves, the client receives a **301 permanent redirect** to it instead of a 404. ## Walking the example The URLconf has `path('events/<slug:slug>/', views.event_detail)`, and a browser requests `/events/launch-party?ref=mail`: 1. Resolution tries `events/launch-party`. The route requires a trailing slash, so nothing matches and the response is a 404. 2. `APPEND_SLASH` is on and the path lacks a slash, so Django checks whether `/events/launch-party/` resolves. It does. 3. The 404 is replaced by a 301 to `/events/launch-party/?ref=mail`; the query string is kept. 4. The browser follows the redirect, the second request resolves, and `event_detail(request, slug='launch-party')` runs. ## Outcomes at a glance | Situation | Result | |---|---| | GET without slash, slashed route exists | 301 to the slashed URL | | POST/PUT/PATCH/DELETE without slash, `DEBUG=True` | `RuntimeError` explaining the redirect cannot keep the data | | POST/PUT/PATCH/DELETE without slash, `DEBUG=False` | 301 sent; the docs warn submitted data may be lost | | Slashless path already matches some pattern | no redirect; that pattern's view runs | | `APPEND_SLASH = False` | plain 404 | | Route written without slash, request has one | plain 404; Django never strips slashes | ## Rules for the URLconf - **End routes with `/` consistently.** The setting can only add a slash. With `path('events/<slug:slug>', ...)`, a request for `/events/launch-party/` is a plain 404. - **No leading slash.** Every request path already starts with `/`. While `APPEND_SLASH` is on, the system check `urls.W002` warns about a route that begins with `/`. - **Build links from route names** with `reverse()` or `{% url %}`, so templates, forms and API clients always use the slashed form and never pay for a redirect. - **Keep catch-alls away from slashless paths.** A `<path:rest>` route at the bottom matches `/events/launch-party` itself, so resolution succeeds and no redirect happens; the catch-all's view answers instead. - **Exempt deliberately.** The decorator `django.views.decorators.common.no_append_slash` marks a view so that requests missing the slash are not redirected to it and stay 404s. ## Unsafe methods and APIs A redirect tells the client to issue a new request, and the request body does not travel with Django's 301. That is why Django refuses to redirect unsafe methods while `DEBUG` is on and raises a `RuntimeError` that names the URL with the slash: it turns a silent production data-loss bug into a loud development error. The usual real-world cause is a hand-written form `action` or an API client that dropped the slash. The fix is on the caller's side, using the reversed URL, rather than adding slashless duplicates of every route. Some teams building JSON APIs choose slashless routes and set `APPEND_SLASH = False`. That is a valid convention as long as it is applied everywhere, because a mix of slashed and slashless routes is exactly what makes the redirect fire unpredictably. ## Why it exists Django's convention is that a URL for a page ends in a slash, like a directory. `APPEND_SLASH` makes hand-typed or truncated links still reach the page while keeping one canonical URL for it, instead of serving the same content at two addresses. ## Verifying it in tests The behaviour is easy to pin down with Django's test client, which runs the configured middleware: - `self.client.get('/events/launch-party')` should return status 301 with a `Location` of `/events/launch-party/`. - `self.client.get('/events/launch-party', follow=True)` should end on the event page with status 200. - A POST to the slashless URL shows which branch applies: Django's test runner sets `DEBUG=False` by default, so the 301 is returned rather than the `RuntimeError`. These tests also guard the route definitions themselves: if someone later writes a route without its trailing slash, or adds a catch-all that swallows slashless paths, the 301 assertion fails.

  • Why does Django raise RuntimeError for a slashless POST under DEBUG instead of redirecting?
    A 301 makes the client send a new request, and the POST body is not guaranteed to survive it, so the submission would silently vanish. With `DEBUG=True` Django raises `RuntimeError` naming the slashed URL so the developer fixes the form or client. With `DEBUG=False` it issues the 301 anyway.
  • In Django, why does a catch-all route at the end of urlpatterns stop APPEND_SLASH redirects?
    The redirect is considered only when the slashless path resolves to nothing. A `<path:rest>` route matches `/events/launch-party` as it stands, so resolution succeeds and the catch-all's view handles the request, typically returning its own 404, and the slashed event page is never offered.

saying these in an interview costs you the question

  • APPEND_SLASH also strips a trailing slash when the route has none.
  • Django redirects every unmatched path by appending a slash, whether or not it then resolves.
  • The APPEND_SLASH redirect re-sends the POST body to the slashed URL.
  • APPEND_SLASH works without CommonMiddleware because the resolver reads it.
  • The slash redirect drops the query string.