Skip to content

Infer controller namespace for link generation when unambiguous#15791

Merged
jdaugherty merged 5 commits into
8.0.xfrom
fix/namespace-aware-link-generation
Jul 7, 2026
Merged

Infer controller namespace for link generation when unambiguous#15791
jdaugherty merged 5 commits into
8.0.xfrom
fix/namespace-aware-link-generation

Conversation

@jamesfredley

@jamesfredley jamesfredley commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

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 same controller/action attributes 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(), and chain().

The Fix

Link generation now infers the namespace from the target controller, so links "just work" from controller and action alone with no namespace attribute in the normal case.

When no namespace is supplied, a shared LinkGenerator.getDefaultNamespace(controller, plugin) resolver decides:

  1. Self-link (target is the controller currently handling the request) -> the current request namespace.
  2. Exactly one controller has the name -> that controller's namespace (namespaced or not). This is the normal case - nothing to specify.
  3. Same name in multiple namespaces (a discouraged design) -> the non-namespaced controller is preferred (matching how dispatch already resolves the collision), then the current namespace; any remaining ambiguity needs an explicit namespace.

An explicit namespace always wins. To target a non-namespaced controller, use namespace="" in GSP markup or namespace: null in a Groovy call (a blank value normalizes to null so 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:

Controllers named car /car/list dispatches to (unchanged) Old link gen New link gen
One, non-namespaced (root) root car /car/list /car/list (unchanged)
One, namespaced (admin) admin car (no-namespace fallback) /car/list - namespace dropped, non-canonical /admin/car/list - canonical
Self-link (page served by admin car) current (admin) car /admin/car/list (already worked) /admin/car/list (unchanged)
Two: root and admin root car (non-namespaced wins the null key) /car/list -> root /car/list -> root (matches dispatch); use namespace="admin" for the other
Two: admin and sales (no root) non-deterministic (last registered wins) whichever won the collision current namespace if the target exists there, else /car/list; explicit namespace required to choose reliably

Explicit selection always works: namespace="admin" -> /admin/car/list; namespace="" / namespace: null -> /car/list.

Implementation

  • New shared resolver on LinkGenerator, implemented in DefaultLinkGenerator, backed by a lazily-built controller-name -> namespaces index that rebuilds when registered controllers change (array-identity invalidation; reset on reload via CachingLinkGenerator.clearCache()).
  • Wired into DefaultLinkGenerator.link(), CachingLinkGenerator cache-key construction (folding the resolved namespace for top-level and nested url shapes and the request context for resource links, so links from different request namespaces never collide), controller chain(), and the g:include / g:sortableColumn tags.
  • ForwardUrlMappingInfo keeps an explicit-namespace-presence flag so g:include can tell an omitted namespace from a deliberate blank/null opt-out when UrlMappingUtils delegates include URL generation back through LinkGenerator.
  • Plugin-targeted links are not inferred from the application's own controllers.

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/namespaces Geb functional specs - every tag, a redirect, a chain, the opt-out, and a custom application tag library that builds links internally.

Docs

grails-doc updated (guide + tag/controller references + What's New + 8.0 upgrade notes) describing the automatic resolution and the namespace="" / namespace: null opt-out.

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
Copilot AI review requested due to automatic review settings June 29, 2026 22:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 in DefaultLinkGenerator, backed by a lazily rebuilt controller-name→namespaces index.
  • Wire namespace inference and explicit-namespace precedence into DefaultLinkGenerator, CachingLinkGenerator cache 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.

@bito-code-review

Copy link
Copy Markdown

The method signature for getDefaultNamespace has been updated to getDefaultNamespace(String controller, String pluginName) to support namespace inference in link generation. The documentation and tests have been updated to reflect this change, ensuring that link generation correctly resolves namespaces automatically when the namespace attribute is omitted.

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

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.58140% with 21 lines in your changes missing coverage. Please review.
✅ Project coverage is 49.5467%. Comparing base (f39f856) to head (114e515).
⚠️ Report is 66 commits behind head on 8.0.x.

Files with missing lines Patch % Lines
...org/grails/web/mapping/DefaultLinkGenerator.groovy 81.6326% 2 Missing and 7 partials ⚠️
.../grails/plugins/web/taglib/UrlMappingTagLib.groovy 42.8571% 4 Missing ⚠️
...y/org/grails/web/mapping/CachingLinkGenerator.java 78.9474% 1 Missing and 3 partials ⚠️
.../main/groovy/grails/web/mapping/LinkGenerator.java 75.0000% 1 Missing and 1 partial ⚠️
...groovy/org/grails/web/mapping/UrlMappingUtils.java 33.3333% 0 Missing and 2 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@               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     
Files with missing lines Coverage Δ
...efact/controller/support/ResponseRedirector.groovy 0.0000% <ø> (ø)
...rg/grails/web/mapping/ForwardUrlMappingInfo.groovy 17.6471% <ø> (ø)
.../main/groovy/grails/web/mapping/LinkGenerator.java 81.8182% <75.0000%> (ø)
...groovy/org/grails/web/mapping/UrlMappingUtils.java 44.9664% <33.3333%> (ø)
.../grails/plugins/web/taglib/UrlMappingTagLib.groovy 60.1307% <42.8571%> (ø)
...y/org/grails/web/mapping/CachingLinkGenerator.java 81.3726% <78.9474%> (ø)
...org/grails/web/mapping/DefaultLinkGenerator.groovy 79.7571% <81.6326%> (ø)

... and 1940 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@testlens-app

testlens-app Bot commented Jul 2, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 114e515
▶️ Tests: 49732 executed
⚪️ Checks: 46/46 completed


Learn more about TestLens at testlens.app.

@jdaugherty jdaugherty merged commit b8122e3 into 8.0.x Jul 7, 2026
49 checks passed
@jdaugherty jdaugherty deleted the fix/namespace-aware-link-generation branch July 7, 2026 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants