Infer controller namespace for link generation when unambiguous#15791
Conversation
Grails controllers can declare a namespace, but URL-generating tags and methods only applied the request namespace when a link targeted the current controller. Links to any other controller dropped the namespace unless it was passed explicitly, so the same controller/action attributes produced namespaced URLs in some places and non-namespaced URLs in others. Link generation now infers the namespace from the target controller so links work from controller and action alone: - a self-link uses the current request namespace - when exactly one controller has the name, that controller's namespace is used - when the same name is defined in multiple namespaces (a discouraged design), the non-namespaced controller is preferred, then the current namespace; any remaining ambiguity must be resolved with an explicit namespace The shared LinkGenerator.getDefaultNamespace resolver is applied consistently to g:link, g:createLink, createLink(), g:form, g:formActionSubmit, g:paginate, g:sortableColumn, g:include, redirect() and chain(), and is folded into the CachingLinkGenerator cache key (including nested url maps and resource links) so links generated from different request namespaces never collide. An explicit namespace always wins; use namespace="" in GSP markup or namespace: null in a method call to target a non-namespaced controller. Adds unit, taglib, controller and functional (Geb) tests covering every surface plus a custom application tag library, and documents the behaviour in grails-doc. Assisted-by: claude-code:claude-4.8-opus
There was a problem hiding this comment.
Pull request overview
This pull request updates Grails’ URL/link generation so that when namespace is not explicitly provided, the framework infers the effective namespace from the target controller (and request context where relevant). This makes namespaced URL generation consistent across link tags/helpers and controller redirects/chains, while still allowing opt-out via namespace="" (GSP) / namespace: null (Groovy).
Changes:
- Add a shared
LinkGenerator.getDefaultNamespace(controller, pluginName)contract and implement it inDefaultLinkGenerator, backed by a lazily rebuilt controller-name→namespaces index. - Wire namespace inference and explicit-namespace precedence into
DefaultLinkGenerator,CachingLinkGeneratorcache key construction,chain(), and relevant GSP taglib paths (g:include,g:sortableColumn). - Add comprehensive unit/functional tests and update user documentation (guide, tag refs, controller refs, and upgrade notes) describing the new inference and opt-out behavior.
Reviewed changes
Copilot reviewed 34 out of 34 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| grails-web-url-mappings/src/main/groovy/grails/web/mapping/LinkGenerator.java | Introduces the shared getDefaultNamespace(controller, pluginName) contract (defaulting to null). |
| grails-web-url-mappings/src/main/groovy/org/grails/web/mapping/DefaultLinkGenerator.groovy | Implements namespace inference, explicit blank→null normalization, and a cached controller namespace index with reset hook. |
| grails-web-url-mappings/src/main/groovy/org/grails/web/mapping/CachingLinkGenerator.java | Updates cache-key construction to include inferred namespace and request context for resource-link shapes; resets namespace index on cache clear. |
| grails-controllers/src/main/groovy/grails/artefact/controller/support/ResponseRedirector.groovy | Updates chain() to honor explicit namespace (including blank/null opt-out) and infer when omitted. |
| grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/UrlMappingTagLib.groovy | Updates g:include and g:sortableColumn namespace handling to align with inference/explicit precedence. |
| grails-web-url-mappings/src/test/groovy/org/grails/web/mapping/nsinference/root/RootNamespaceControllers.groovy | Adds root controllers for inference test fixtures. |
| grails-web-url-mappings/src/test/groovy/org/grails/web/mapping/nsinference/admin/AdminNamespaceControllers.groovy | Adds admin-namespaced controllers for inference test fixtures. |
| grails-web-url-mappings/src/test/groovy/org/grails/web/mapping/nsinference/sales/SalesNamespaceControllers.groovy | Adds sales-namespaced controller for inference test fixtures. |
| grails-web-url-mappings/src/test/groovy/org/grails/web/mapping/LinkGeneratorNamespaceInferenceSpec.groovy | Adds exhaustive unit tests for resolver/link behavior, cache collision regressions, plugin guard, and blank namespace normalization. |
| grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/nsinference/root/RootNamespaceControllers.groovy | Adds root controllers for taglib inference tests. |
| grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/nsinference/admin/AdminNamespaceControllers.groovy | Adds admin controllers for taglib inference tests. |
| grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/nsinference/sales/SalesNamespaceControllers.groovy | Adds sales controller for taglib inference tests. |
| grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/NamespaceInferenceTagLibSpec.groovy | Adds unit tests covering URL-generating GSP tags and opt-out semantics. |
| grails-test-suite-uber/src/test/groovy/org/grails/web/servlet/mvc/beta/NamespacedController.groovy | Adds controller actions to exercise chain() namespace inference/override behavior. |
| grails-test-suite-uber/src/test/groovy/org/grails/web/servlet/mvc/RedirectMethodTests.groovy | Extends redirect/chain tests to cover inferred namespace, explicit namespace, and explicit-null opt-out. |
| grails-test-examples/namespaces/grails-app/controllers/UrlMappings.groovy | Adds a frontend namespace mapping for functional coverage. |
| grails-test-examples/namespaces/grails-app/controllers/namespaces/admin/PageController.groovy | Adds actions rendering link/tag examples and exercising redirect/chain behaviors. |
| grails-test-examples/namespaces/grails-app/controllers/namespaces/admin/BookController.groovy | Adds a namespaced Book controller used by functional specs. |
| grails-test-examples/namespaces/grails-app/views/page/namespaceLinks.gsp | Adds a view that renders multiple URL-generating tags covering inference and opt-out. |
| grails-test-examples/namespaces/grails-app/views/book/index.gsp | Adds a simple view used by functional navigation assertions. |
| grails-test-examples/namespaces/grails-app/taglib/namespaces/AppLinkTagLib.groovy | Adds an app taglib that internally builds links (functional coverage of app-provided taglibs). |
| grails-test-examples/namespaces/src/integration-test/groovy/namespaces/NamespaceViewRenderingSpec.groovy | Adds functional (Geb) assertions covering tag URLs, redirects, chains, and opt-out behavior end-to-end. |
| grails-doc/src/en/ref/Tags - GSP/link.adoc | Documents namespace inference + opt-out for g:link. |
| grails-doc/src/en/ref/Tags - GSP/createLink.adoc | Documents namespace inference + opt-out for g:createLink and fixes a typo in an example comment. |
| grails-doc/src/en/ref/Tags - GSP/form.adoc | Documents namespace inference + opt-out for g:form. |
| grails-doc/src/en/ref/Tags - GSP/paginate.adoc | Documents namespace inference + opt-out for g:paginate. |
| grails-doc/src/en/ref/Tags - GSP/sortableColumn.adoc | Documents namespace inference + opt-out for g:sortableColumn. |
| grails-doc/src/en/ref/Tags - GSP/include.adoc | Documents namespace inference + opt-out for g:include. |
| grails-doc/src/en/ref/Controllers/redirect.adoc | Updates redirect docs to reflect automatic namespace resolution + opt-out. |
| grails-doc/src/en/ref/Controllers/chain.adoc | Updates chain docs to reflect automatic namespace resolution + opt-out. |
| grails-doc/src/en/ref/Controllers/namespace.adoc | Adds documentation that namespace can be resolved from the target controller when omitted. |
| grails-doc/src/en/guide/theWebLayer/urlmappings/namespacedControllers.adoc | Updates guide to describe the new resolution rules and discouraged ambiguity cases. |
| grails-doc/src/en/guide/introduction/whatsNew.adoc | Adds “Namespace-Aware Link Generation” release note section. |
| grails-doc/src/en/guide/upgrading/upgrading80x.adoc | Adds upgrade note explaining new behavior and opt-out syntax. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
The method signature for |
Keep the explicit namespace presence flag on forward mapping info so g:include can distinguish omitted namespace values from deliberate blank or null namespace opt-outs when UrlMappingUtils delegates include URL generation back through LinkGenerator. Assisted-by: opencode:gpt-5.5
Document that the namespace test fixture exercises statically declared controller namespaces, while request-time mapping conditions still require explicit namespace selection when ambiguous. Assisted-by: opencode:gpt-5.5
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## 8.0.x #15791 +/- ##
================================================
+ Coverage 0 49.5467% +49.5467%
- Complexity 0 16752 +16752
================================================
Files 0 1947 +1947
Lines 0 92549 +92549
Branches 0 16172 +16172
================================================
+ Hits 0 45855 +45855
- Misses 0 39569 +39569
- Partials 0 7125 +7125
🚀 New features to boost your workflow:
|
✅ All tests passed ✅🏷️ Commit: 114e515 Learn more about TestLens at testlens.app. |
The Problem
Grails controllers can declare a
namespace, but URL generation only applied the request namespace when a link targeted the current controller. A link to any other controller silently dropped the namespace unless it was passed explicitly - so the samecontroller/actionattributes produced namespaced URLs in some places and non-namespaced URLs in others.This inconsistency spanned
g:link,g:createLink,g:form,g:paginate,g:sortableColumn,g:include,redirect(), andchain().The Fix
Link generation now infers the namespace from the target controller, so links "just work" from
controllerandactionalone with nonamespaceattribute in the normal case.When no
namespaceis supplied, a sharedLinkGenerator.getDefaultNamespace(controller, plugin)resolver decides:namespace.An explicit
namespacealways wins. To target a non-namespaced controller, usenamespace=""in GSP markup ornamespace: nullin a Groovy call (a blank value normalizes tonullso it matches non-namespaced reverse mappings).Dispatch alignment
This does not change request dispatch (which controller serves a URL); it makes link generation produce the URL dispatch already resolves to. Linking
controller="car" action="list"with no explicit namespace:car/car/listdispatches to (unchanged)car/car/list/car/list(unchanged)admin)admincar(no-namespace fallback)/car/list- namespace dropped, non-canonical/admin/car/list- canonicaladmincar)admin)car/admin/car/list(already worked)/admin/car/list(unchanged)admincar(non-namespaced wins the null key)/car/list-> root/car/list-> root (matches dispatch); usenamespace="admin"for the otheradminandsales(no root)/car/list; explicitnamespacerequired to choose reliablyExplicit selection always works:
namespace="admin"->/admin/car/list;namespace=""/namespace: null->/car/list.Implementation
LinkGenerator, implemented inDefaultLinkGenerator, backed by a lazily-built controller-name -> namespaces index that rebuilds when registered controllers change (array-identity invalidation; reset on reload viaCachingLinkGenerator.clearCache()).DefaultLinkGenerator.link(),CachingLinkGeneratorcache-key construction (folding the resolved namespace for top-level and nestedurlshapes and the request context for resource links, so links from different request namespaces never collide), controllerchain(), and theg:include/g:sortableColumntags.ForwardUrlMappingInfokeeps an explicit-namespace-presence flag sog:includecan tell an omitted namespace from a deliberate blank/null opt-out whenUrlMappingUtilsdelegates include URL generation back throughLinkGenerator.Tests
LinkGeneratorNamespaceInferenceSpec- exhaustive resolver +link()cases, cache-collision regressions (url-map and resource), plugin guard, blank-namespace normalization.NamespaceInferenceTagLibSpec- every URL-generating GSP tag.RedirectMethodTests-chain()namespace cases.grails-test-examples/namespacesGeb functional specs - every tag, a redirect, a chain, the opt-out, and a custom application tag library that builds links internally.Docs
grails-docupdated (guide + tag/controller references + What's New + 8.0 upgrade notes) describing the automatic resolution and thenamespace=""/namespace: nullopt-out.