When a reverse proxy serves a Django project under /shop/, how does Django learn that prefix so reverse() and redirects include it?
answer
- the prefix stays out of urls.py
- script name vs path info
- root_path in the ASGI scope
- one setting that forces it
basics
~20 sDjango 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 sKeep `/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# 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 proxygo deeper
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.
Explain SCRIPT_NAME and PATH_INFO under WSGI, root_path under ASGI, and what FORCE_SCRIPT_NAME overrides.
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.
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()