Opened 4 years ago

Closed 4 years ago

Last modified 4 years ago

#31724 closed Bug (invalid)

django.urls.resolve ignores the FORCE_SCRIPT_NAME setting.

Reported by: Pēteris Caune Owned by: nobody
Component: Core (URLs) Version: 3.0
Severity: Normal Keywords:
Cc: Florian Apolloner Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by Pēteris Caune)

I suspect the django.urls.resolve function is not taking in account the FORCE_SCRIPT_NAME setting.

First, with FORCE_SCRIPT_NAME=None, I can do the following in manage.py shell:

>>> from django.urls import resolve, reverse
>>> reverse("hc-docs")
'/docs/'
>>> resolve(reverse("hc-docs"))
ResolverMatch(func=hc.front.views.serve_doc, args=(), kwargs={}, url_name=hc-docs, app_names=[], namespaces=[], route=docs/)

In the above example, I can feed the result of reverse back into resolve and it works as expected.

Now with FORCE_SCRIPT_NAME='/foo':

>>> from django.urls import resolve, reverse
>>> reverse("hc-docs")
'/foo/docs/'
>>> resolve(reverse("hc-docs"))
Traceback (most recent call last):
(...)
    raise Resolver404({'tried': tried, 'path': new_path})

Here, reverse added the "/foo" prefix to the URL, but resolve didn't seem to recognize it.

I suspect this is a bug but not sure. How is django.urls.resolve expected to behave here?

Change History (5)

comment:1 by Pēteris Caune, 4 years ago

Description: modified (diff)

comment:2 by Mariusz Felisiak, 4 years ago

Cc: Florian Apolloner added
Resolution: duplicate
Status: newclosed
Summary: django.urls.resolve ignores the FORCE_SCRIPT_NAME settingdjango.urls.resolve ignores the FORCE_SCRIPT_NAME setting.
Type: UncategorizedBug

I'm pretty sure that's a duplicate of #7930, resolve()/reserve() is behavior is discussed there.

comment:3 by Florian Apolloner, 4 years ago

Resolution: duplicateinvalid

Thank you for the CC, I disagree on the duplicate state though and will reresolve it as invalid.

I think that for this issue FORCE_SCRIPT_NAME works as documented and designed. The example wrongly assumes that resolve & reverse are able to round-trip like that. This is simply not the case: resolve does not operate on full URLs, but on PATH_INFO -- https://github.com/django/django/blob/27c09043da52ca1f02605bf28600bfd5ace95ae4/django/core/handlers/base.py#L288 . So assuming your webserver is configured to serve Django under /foo/ and you access /foo/docs/ in the browser, the actual URL that Django sees to resolve the view is /docs/. It is able to do so because the webserver did configure SCRIPT_NAME as /foo/ effectively telling Django that only the part after it (ie PATH_INFO) is relevant. But to reverse a URL properly Django does need to prepend the SCRIPT_NAME (or FORCE_SCRIPT_NAME for that matter).

All in all FORCE_SCRIPT_NAME serves two specific purposes:

  • If your webserver configuration is broken, FORCE_SCRIPT_NAME can be used to correct the value of SCRIPT_NAME (though generally that might cause wrong PATH_INFO as well, so I wouldn't really recommend it in this scenario and try to fix the webserver).
  • If you need to generate URLs to your views outside of a request context. That is for example a cron job that runs without a WSGI environment (so SCRIPT_NAME is never set) and the generated URL would miss the prefix. In this case you will need to set FORCE_SCRIPT_NAME to the actual SCRIPT_NAME that your webserver would report, so Django knows about it in management commands etc… Under normal operations (ie when serving views) the webserver is able to provide that information on it's own.

comment:4 by Pēteris Caune, 4 years ago

Thanks for the extra detail, Florian!

Perhaps the documentation should mention that reverse will obey FORCE_SCRIPT_NAME but resolve won't? They are both in the same module, they are documented on the same page and they look like inverse operations – I knew reverse uses FORCE_SCRIPT_NAME and that's why I assumed resolve would as well.

My use case: I'm using resolve for whitelisting redirect URLs. In my app, the login page can take a ?next=some-url GET parameter, and after the user logs in, they get redirected to some-url. I wanted to have a whitelist of what redirect targets are allowed. So I'm passing some-url to resolve and am looking if the ResolverMatch.url_name is in my whitelist. Does this seem like a reasonable use case for resolve?

And I'm using FORCE_SCRIPT_NAME setting because indeed I need to generate URLs from outside of a request context (management commands sending emails with links in them).

in reply to:  4 comment:5 by Florian Apolloner, 4 years ago

Replying to Pēteris Caune:

Perhaps the documentation should mention that reverse will obey FORCE_SCRIPT_NAME but resolve won't?

Documentation updates will never hurt. Do you feel up to the task? :)

My use case: I'm using resolve for whitelisting redirect URLs. In my app, the login page can take a ?next=some-url GET parameter, and after the user logs in, they get redirected to some-url. I wanted to have a whitelist of what redirect targets are allowed. So I'm passing some-url to resolve and am looking if the ResolverMatch.url_name is in my whitelist. Does this seem like a reasonable use case for resolve?

It does, but sadly is not something that is achievable easily as of now, I am sorry :( I agree that it is unfortunate that resolve behaves like it does, but it makes a little bit more sense with the SCRIPT_NAME/PATH_INFO differences from above. If Django were to pass SCRIPT_NAME + PATH_INFO to resolve then the urlpatterns would have to include SCRIPT_NAME and as such would no longer be portable. As far as Django is concerned it is rooted below SCRIPT_NAME. Given how html links work the only sensible option for reverse is to generate a path absolute URL which requires it to append the SCRIPT_NAME. It would be even worse if Django supported hosting on multiple subdomains, then reverse would most likely have to generate full URLs including domains ;)

As for your issue at hand, I think the easiest solution is to check if your URL starts with SCRIPT_NAME/FORCE_SCRIPT_NAME and then cut that off.

Note: See TracTickets for help on using tickets.
Back to Top