skip to content

A Django comments app is mounted at /news/comments/ and /support/comments/, and posting a news comment redirects into the support section — why, and how do you fix it?

level: seniorimportance: nice to knowfreq 22%

answer

  1. templates are right, redirects wrong
  2. which lookups know the request
  3. fallback when nothing is current
  4. an argument redirect() lacks
  5. class attributes run at import

basics

~10 s

The view's redirect('comments:list') reverses with no current app, so Django falls back to the default instance or, lacking one, the last included mount: support. Reverse with current_app=request.resolver_match.namespace and redirect to that URL.

solid answer

~40 s

Only `{% url %}` fills in the current app from the request; `reverse()`, `reverse_lazy()`, `redirect('comments:list')` and a model's `get_absolute_url()` do not. With no current app and no instance named `comments`, Django uses the last deployed instance, so every Python-built URL points at `support-comments`. Fix it where the URL is built: `reverse('comments:list', current_app=request.resolver_match.namespace)` and then `redirect(url)`. `redirect()` has no `current_app` parameter; extra keywords are passed to `reverse()` as URL arguments. In class-based views, replace a class-level `success_url = reverse_lazy(...)` with `get_success_url()` using `self.request`. Optionally make one mount the default instance so fallbacks are predictable, and add a test per mount asserting the redirect's target.

go deeper

for a junior

Recall that the same app mounted twice needs its links to stay in the section the user is in, and that templates handle this automatically.

for a middle

Explain that only {% url %} fills the current app from the request, so reverse() and redirect() fall back to the default or last deployed instance.

for a senior

Diagnose cross-section redirects quickly, fix them with current_app and get_success_url(), and harden the app with per-mount tests and a deliberate default instance.

for a principal

Decide whether reusable apps should accept a request-aware URL helper as policy, so no contributor can build a namespaced URL without stating the instance.

## The symptom The comments app is included twice: ```python # mysite/urls.py from django.urls import include, path urlpatterns = [ path('news/comments/', include('comments.urls', namespace='news-comments')), path('support/comments/', include('comments.urls', namespace='support-comments')), ] ``` Links in the comment pages are correct in both sections, but after posting a comment under `/news/comments/`, the user lands on `/support/comments/`. The bug is in the view: ```python from django.shortcuts import redirect def comment_create(request): ... # save the comment return redirect('comments:list') ``` ## Why templates work and the redirect does not Django resolves `'comments:list'` in a fixed order: the **current app**, then the **default instance** (instance namespace equal to `comments`), then the **last deployed** instance. What differs between call sites is whether a current app is supplied: | Call site | Current app supplied? | Result here | |---|---|---| | `{% url 'comments:list' %}` in a template rendered with the request | yes: `request.current_app`, else `request.resolver_match.namespace` | the serving section | | `reverse('comments:list')` | no | last deployed: support | | `redirect('comments:list')` | no: it calls `reverse()` without one | last deployed: support | | `success_url = reverse_lazy('comments:list')` on a view class | no request exists at class definition | last deployed: support | | `Comment.get_absolute_url()` using `reverse()` | no request available | last deployed: support | There is no default instance, and support is declared last, so every Python-built URL points at support. ## Fixing function views ```python from django.shortcuts import redirect from django.urls import reverse def comment_create(request): ... # save the comment url = reverse('comments:list', current_app=request.resolver_match.namespace) return redirect(url) ``` `redirect()` itself has **no** `current_app` parameter. Its extra keyword arguments are forwarded to `reverse()` as URL kwargs, so `redirect('comments:list', current_app=...)` fails with `NoReverseMatch`. Build the URL first, then redirect to it. ## Fixing class-based views A class attribute such as `success_url = reverse_lazy('comments:list')` is evaluated without a request, so it cannot know the section. Move the lookup into the method that runs per request: ```python from django.urls import reverse from django.views.generic.edit import CreateView from .models import Comment class CommentCreateView(CreateView): model = Comment fields = ['body'] def get_success_url(self): return reverse( 'comments:detail', kwargs={'pk': self.object.pk}, current_app=self.request.resolver_match.namespace, ) ``` ## Hardening the app and the project 1. **Grep the app for every Python `reverse()`, `reverse_lazy()` and `redirect()` by name** and give each a current app, or confine URL building to templates rendered with the request. 2. **Treat `get_absolute_url()` as section-blind.** A model has no request; if the section is stored on the comment, reverse with that section's instance namespace instead. 3. **Choose a default instance.** Mounting news with the instance namespace `comments` makes fallbacks go to news rather than to whichever include is last, and adding a third mount later no longer changes where old fallbacks go. 4. **Test both mounts.** Post a comment through `/news/comments/` and `/support/comments/` in tests and assert each response's `Location` header points back into its own section. ## Where else the same bug hides Any code path that builds a namespaced URL without the request falls back the same way: - **Emails and notifications** rendered with `render_to_string()` and no request, such as a reply notification linking to the thread. - **Background jobs and management commands** that build links, where no request exists at all. - **API responses** that include `reverse()`-built links to comments. - **Sitemaps and feeds** that call `get_absolute_url()` on each comment. - **Tests** that reverse names without `current_app` and therefore silently assert the fallback instance. For these, store which section an object belongs to and reverse with that section's instance namespace, rather than relying on a current app that does not exist. ## Why Django behaves this way `reverse()` is a plain function that can be called anywhere, including at import time, in management commands and in background jobs, where no request exists. Django therefore requires the caller to state the current app explicitly. The template tag is the one place where the request is normally at hand, so it fills the value in. Django's admin, which supports several `AdminSite` instances under the shared `admin` application namespace, follows exactly this discipline and reverses its URLs with `current_app` set to its site's name.

  • Would setting request.current_app in a middleware fix the Django redirect?
    No. `request.current_app` is read only by the `{% url %}` tag. `reverse()` and `redirect()` never look at the request, so Python code must still pass `current_app` explicitly. The attribute is useful for steering templates, not for URLs built in views.
  • How would you catch this Django namespace bug before it reaches production?
    Write one test per mount that submits the form under that prefix and asserts the redirect's `Location` starts with the same prefix. Also reverse a few names from code with no current app in a test, which exposes whether fallbacks go to an intentional default instance.

saying these in an interview costs you the question

  • redirect('comments:list') uses the namespace of the request that is being handled.
  • Passing current_app= to redirect() fixes the lookup.
  • With no default instance, Django falls back to the first included mount.
  • A class-level success_url built with reverse_lazy() can use the current request's namespace.
  • Setting request.current_app makes reverse() in views use that instance.