skip to content

When a reverse proxy serves a Django project under /shop/, how does Django learn that prefix so reverse() and redirects include it?

level: seniorimportance: nice to knowfreq 20%

answer

  1. the prefix stays out of urls.py
  2. script name vs path info
  3. root_path in the ASGI scope
  4. one setting that forces it

basics

~20 s

Django takes a script prefix per request from the WSGI SCRIPT_NAME or the ASGI root_path, or from FORCE_SCRIPT_NAME when set, and reverse() prepends it. The server must pass the prefix that way rather than baking /shop/ into the URLconf.

solid answer

~40 s

Keep `/shop/` out of `urls.py`. For each request the WSGI handler calls `set_script_prefix()` with the environ's `SCRIPT_NAME` (the ASGI handler uses the scope's `root_path`), and URL resolution runs on the path after that prefix. `reverse()`, `{% url %}` and `redirect()` given a view name then prepend the prefix automatically, and a relative `STATIC_URL` such as the default `'static/'` gets it too. So configure the app server or proxy to deliver the prefix as `SCRIPT_NAME` or `root_path`; if it cannot, set `FORCE_SCRIPT_NAME = '/shop'`, which overrides what the server sends. Under WSGI, `PATH_INFO` must still arrive without the prefix; under ASGI, Django strips a matching prefix from the path itself. The symptom of getting it wrong is links and redirects to `/orders/` instead of `/shop/orders/`.

code

python · 10 lines
python
# settings/production.py — only when the server cannot pass SCRIPT_NAME or root_path
FORCE_SCRIPT_NAME = '/shop'
STATIC_URL = 'static/'  # relative: becomes /shop/static/

# shop/views.py
from django.shortcuts import redirect


def checkout_done(request):
    return redirect('orders:list')  # resolves to /shop/orders/ behind the proxy

go deeper

for a junior

Know that Django projects can be mounted under a sub-path without changing urls.py, as long as URLs are built with reverse() or the url tag.

for a middle

Explain SCRIPT_NAME and PATH_INFO under WSGI, root_path under ASGI, and what FORCE_SCRIPT_NAME overrides.

for a senior

Diagnose 404s and escaping redirects behind a proxy by checking which part of the path reaches Django, and fix the proxy or server configuration rather than the URLconf.

for a principal

Decide whether services should share a host under sub-paths at all, given the configuration each mount adds to proxies, static URLs and cookies.

## The problem A reverse proxy often publishes several applications under one host: `/shop/` goes to a Django project, `/blog/` to something else. The Django project's URLconf is written as if it lived at `/`: `path('orders/', ...)`. Two things must then be true: - incoming URLs must be **resolved without** the `/shop` part, or no pattern matches; - generated URLs must be **built with** it, or every link and redirect escapes the mount point. Hard-coding `shop/` into every pattern solves the first half badly and breaks the moment the mount point changes. Django has a mechanism for this instead: the **script prefix**. ## How Django gets the prefix The prefix is set **per request** by the handler, not at start-up (which is why the entry-point factories call `django.setup(set_prefix=False)`): 1. **WSGI:** the handler calls `set_script_prefix()` with the request's script name, taken from the environ's `SCRIPT_NAME` (or reconstructed from `SCRIPT_URL`/`REDIRECT_URL` after an Apache rewrite). URL resolution uses `PATH_INFO`, which the server must deliver **without** the prefix. 2. **ASGI:** the handler takes the prefix from the scope's `root_path`; if the request path starts with that prefix, Django removes it to get the path it resolves. 3. **`FORCE_SCRIPT_NAME`:** when this setting is not `None` (its default is `None`), it replaces whatever the server sent, under both protocols. How the prefix reaches `SCRIPT_NAME` or `root_path` is a property of the application server and proxy configuration, and differs between servers. ## What uses the prefix | Feature | Behaviour with the prefix set | |---|---| | `reverse('orders:list')` | returns `/shop/orders/` | | `{% url 'orders:list' %}` | renders `/shop/orders/` | | `redirect('orders:list')` | redirects to `/shop/orders/` | | relative `STATIC_URL = 'static/'` | becomes `/shop/static/` | | absolute `STATIC_URL = '/static/'` | left as is | | a hard-coded `'/orders/'` string | left as is, so it escapes the mount point | The relative `STATIC_URL`/`MEDIA_URL` behaviour is why new projects default to `STATIC_URL = 'static/'`: Django prefixes relative values with the script prefix. ## Choosing a configuration - **Best:** the proxy and app server pass the prefix as `SCRIPT_NAME` or `root_path` and forward the rest of the path. Django then needs no setting, and the same build runs at `/` in development. - **Fallback:** set `FORCE_SCRIPT_NAME = '/shop'` in production settings when the server cannot pass the prefix. Remember that under WSGI the proxy must still strip `/shop` from the forwarded path so `PATH_INFO` matches the URLconf. - **Avoid:** adding `shop/` to URL patterns, or `include()`-ing the whole URLconf under `shop/`. It resolves requests but ties the code to one mount point and does nothing for static URLs set elsewhere. ## Symptoms that point here - every page 404s behind the proxy but works on the app server directly: the prefix is reaching `PATH_INFO`; - pages load but links, form actions and login redirects jump to the host root: the prefix never reached Django; - the admin works but its CSS does not: `STATIC_URL` is absolute, or static files are published elsewhere. ## What interviewers listen for That the prefix is a per-request script name rather than part of the URLconf, the difference between `SCRIPT_NAME`/`root_path` and `FORCE_SCRIPT_NAME`, and that only URLs built through Django's reversing machinery pick it up.

  • Behind the proxy every URL returns 404, although the app works when hit directly. What is the likely cause under WSGI?
    The prefix is reaching Django inside `PATH_INFO`, so the resolver sees `/shop/orders/` and no pattern matches. Under WSGI Django resolves `PATH_INFO` as delivered, even when `FORCE_SCRIPT_NAME` is set. Make the proxy strip `/shop` from the forwarded path, or have the app server split it into `SCRIPT_NAME`.
  • Why does the default STATIC_URL in new projects have no leading slash?
    Django prefixes a relative `STATIC_URL` or `MEDIA_URL` with the current script prefix, so `'static/'` becomes `/shop/static/` when the project is mounted under `/shop`. An absolute value such as `'/static/'` or a full URL is used unchanged, which suits a CDN or a proxy that serves static files at the root.

saying these in an interview costs you the question

  • You must add the shop/ prefix to every URL pattern
  • FORCE_SCRIPT_NAME also makes Django strip the prefix from PATH_INFO under WSGI
  • Hard-coded '/orders/' strings pick up the script prefix automatically
  • An absolute STATIC_URL such as '/static/' is prefixed with SCRIPT_NAME
  • The script prefix is fixed once at startup by django.setup()