Contexto
El paquete blueprints.urls estaba incluido cuatro veces en config/urls.py:
blueprints/(include con barra)blueprints(include sin barra)maps/(include con barra)maps(include sin barra)
El problema: includes sin barra
Cuando Django incluye un URLconf bajo un prefijo sin barra final, concatena los paths hijos directamente sin separador. Esto genera rutas absurdas pero alcanzables:
/blueprints5(siblueprints.urlstiene una ruta<int:id>)/mapseditor/3(si existeeditor/<int:id>)
Peor: reverse() devolvía ESAS rutas pegadas:
reverse("blueprint_detail", args=[3]) # → "/maps3/" (antes)
reverse("blueprint_editor", args=[3]) # → "/mapseditor/3/" (antes)
Alcance (por qué no saltó a producción)
El fallo era latente:
- Los enlaces de las plantillas se escriben a mano (
href="/maps/{{ id }}") blueprint_detailyblueprint_editorno se usaban con{% url %}en ninguna plantilla- El único que sí se usaba era
blueprints_index, que devolvía/maps(sin barra pero funcional)
La primera pantalla que pidiera {% url "blueprint_detail" args=[3] %} habría devuelto /maps3/ al cliente.
Cómo se destapó
Lo descubrió el mapa de superficie generado en v1.66.6: al listar todas las URLs alcanzables leyendo el código estático, encontró rutas que nadie habría escrito a mano. La herramienta encontró algo en su primer uso real.
Decisión: RedirectView + query_string=True
Sustituir los dos include() sin barra por redirecciones explícitas:
path("blueprints/", include("blueprints.urls")),
path("maps/", include("blueprints.urls")),
path("blueprints", RedirectView.as_view(url="/blueprints/", query_string=True)),
path("maps", RedirectView.as_view(url="/maps/", query_string=True)),
Por qué redirecciones y no borrar directamente
APPEND_SLASH está desactivado a propósito en settings/base.py:
- Sin él, una petición a
/blueprintsdevolvería 404 (sin redirección automática) - Existe navegación viva en el código (
static/js/blueprints/MapInteraction.js) que accede a/blueprints?show_list=true - La redirección debe conservar la query:
query_string=Trueasegura que/blueprints?show_list=true→/blueprints/?show_list=true
Contrato fijado por tests
Se añade tests/test_urls_blueprints_prefixes.py con 4 tests parametrizados:
test_la_url_generada_esta_bien_formada:reverse()devuelve URLs bien separadas (/maps/o/blueprints/, nunca pegadas)test_la_url_generada_resuelve_de_vuelta: la URL generada resuelve correctamente conresolve()test_el_prefijo_sin_barra_redirige_y_conserva_la_query:/blueprintsy/mapsredirigen 301/302 conservando querytest_las_rutas_concatenadas_ya_no_existen:/blueprints5,/mapseditor/3devuelven 404
Implicaciones arquitectónicas
Este patrón de error (include sin barra) aparece en muchas Django apps. La decisión documenta:
- Anti-patrón: no incluir URLconf sin barra final (aunque sea sintácticamente válido)
- Patrón seguro: siempre barra final en
include()+ redirecciones explícitas si es necesario - Conservación de query:
query_string=TrueenRedirectViewpara no perder parámetros de navegación - Testing: parametrizar tests de URLs para asegurar ida/vuelta y no regresiones silenciosas
Véase también
- [[feature—config—blueprints-maps-url-cleanup]]
- [[concept—config—django-url-patterns]]