Goal
Understand what {% url %} and reverse() return once hostmap is
installed, so you can rely on them the same way you always have, in
templates and in Python, without an explicit host at any call site.
Prerequisites
HOSTMAPconfigured with at least two entries.HOSTMAP_PATCH_REVERSEat its default (True). If it isFalse, see turn off reversing instead: stockreverse()is unpatched and only the explicit API is host-aware.
Steps
1. Use {% url %} exactly as you always have
No template changes, no new tag, no host= argument:
{# templates/nav.html #}
<a href="{% url 'blog-index' %}">Blog</a>
<a href="{% url 'api:user-detail' request.user.pk %}">My API profile</a>
If blog-index resolves on the active host, you get a relative path
(/blog/), byte-identical to what stock Django would render. If
api:user-detail only resolves on the api host, you get an absolute URL
(https://api.example.com/users/7/). The template author does not need to
know, or care, which case applies.
2. Use reverse() exactly as you always have in Python
from django.urls import reverse
reverse("blog-index") # '/blog/'
reverse("api:user-detail", args=[7]) # 'https://api.example.com/users/7/'
This is stock django.urls.reverse, not a hostmap wrapper. It works
identically inside your own views and inside third-party apps you have not
touched, because hostmap hooks resolver acquisition rather than rebinding
reverse itself. See
how host-aware reversing works
for the mechanism.
3. Know the silent-failure contract still holds
{% url ... as var %} keeps Django's usual silent-failure behaviour through
the cross-host fallback: if the name resolves on no host at all, the
template assigns nothing to var rather than raising.
{% url 'maybe-missing' as maybe_link %}
{% if maybe_link %}<a href="{{ maybe_link }}">Link</a>{% endif %}
Without as, a name that resolves on no configured host raises
NoReverseMatch, naming every host that was searched, exactly like stock
Django names the URLconf it searched.
4. Know when a result is absolute versus relative
Anything downstream that already tolerates an absolute URL from reverse()
continues to work unmodified: HttpResponseRedirect, the redirect()
shortcut, resolve_url(), and request.build_absolute_uri() all accept
absolute input. If you have code that assumes reverse() always returns a
path (for example, string concatenation like "/prefix" + reverse(...)),
that assumption was already fragile; hostmap surfaces it rather than causing
it.
Verify it worked
import pytest
from django.urls import reverse
from hostmap import context
@pytest.mark.django_db
def test_same_host_reverse_is_relative(settings):
assert reverse("blog-index") == "/blog/"
@pytest.mark.django_db
def test_cross_host_reverse_is_absolute(settings):
url = reverse("api:user-detail", args=[7])
assert url.startswith("https://api.")
Use host_client() to exercise this through a real
request against a specific host.
Common pitfalls
- Assuming
reverse()always returns a path. It may return an absolute URL once a cross-host name is reversed. Code that concatenates ontoreverse()output should be audited once, the same way you would audit any library upgrade that changes a return type. - Expecting
{% url %}to raise insideas. It never does; that is stock Django template behaviour, unchanged. - Forgetting the active host is what "same-host" means. "Same-host" is relative to whichever entry is active for the current reverse call, not necessarily the entry the template happens to render under. Out of a request, the active host is the default entry; see resolution order.
Related
- How host-aware reversing works: the resolver-acquisition seam and why third-party apps need no changes.
- Resolution order: the exact order cross-host fallback searches in.
- Reverse out of a request: pinning the
active host explicitly with
use_host().