From 1fb571d6e268808c9cb3154e155b5061dd19fbb6 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 28 Aug 2026 11:03:25 -0600 Subject: [PATCH 1/5] fix: preserve domains in nested route resolution --- system/web/services/RoutingService.cfc | 2 + .../specs/web/routing/RoutingServiceTest.cfc | 48 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/system/web/services/RoutingService.cfc b/system/web/services/RoutingService.cfc index 8b37821f9..3f70dbc52 100644 --- a/system/web/services/RoutingService.cfc +++ b/system/web/services/RoutingService.cfc @@ -634,6 +634,7 @@ component extends="coldbox.system.web.services.BaseService" accessors="true" { // build routing argument struct based on module/namespace context var contextRouting = { action : reReplaceNoCase( requestString, results.route.regexpattern, "" ), + domain : arguments.domain, event : arguments.event, excludedPatterns : arguments.excludedPatterns }; @@ -695,6 +696,7 @@ component extends="coldbox.system.web.services.BaseService" accessors="true" { // Return found Route recursively. return findRoute( action = packagedRequestString, + domain = arguments.domain, event = arguments.event, module = arguments.module, excludedPatterns = arguments.excludedPatterns diff --git a/tests/specs/web/routing/RoutingServiceTest.cfc b/tests/specs/web/routing/RoutingServiceTest.cfc index eaf516794..77a3f89d8 100755 --- a/tests/specs/web/routing/RoutingServiceTest.cfc +++ b/tests/specs/web/routing/RoutingServiceTest.cfc @@ -281,6 +281,54 @@ expect( discoveredEventPOST ).toBe( "api-v1:MyOtherHandler.create" ); } ); + + it( "preserves the request domain when resolving module routes", function(){ + var moduleName = "domainRoutingTest"; + var router = getController().getRoutingService().getRouter(); + var modules = getController().getSetting( "modules" ); + var mockEvent = createMock( "coldbox.system.web.context.RequestContext" ).init( + controller = getController(), + properties = { + defaultLayout : "Main.cfm", + defaultView : "", + eventName : "event", + modules : {} + } + ); + + modules[ moduleName ] = { resources : [], routes : [] }; + + try { + router.addModuleRoutes( + pattern = "/domain-module", + module = moduleName, + append = false + ); + router.addRoute( + pattern = "/ceremony", + event = "Passkeys.authenticate", + domain = "allowed.example", + module = moduleName + ); + + var allowed = routingService.findRoute( + action = "/domain-module/ceremony", + domain = "allowed.example", + event = mockEvent + ); + var denied = routingService.findRoute( + action = "/domain-module/ceremony", + domain = "denied.example", + event = mockEvent + ); + + expect( allowed.route.event ).toBe( "Passkeys.authenticate" ); + expect( denied.route.event ).notToBe( "Passkeys.authenticate" ); + } finally { + router.removeModuleRoutes( moduleName ); + structDelete( modules, moduleName ); + } + } ); } ); describe( "route-scoped middleware (runRouteMiddleware())", function(){ From 1618d7b94a53051326112ded04d2ecaa595907da Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 28 Aug 2026 11:12:20 -0600 Subject: [PATCH 2/5] fix: reject unmatched nested route mounts --- system/web/services/RoutingService.cfc | 8 ++++---- tests/specs/web/routing/RoutingServiceTest.cfc | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/system/web/services/RoutingService.cfc b/system/web/services/RoutingService.cfc index 3f70dbc52..f51a70533 100644 --- a/system/web/services/RoutingService.cfc +++ b/system/web/services/RoutingService.cfc @@ -660,10 +660,10 @@ component extends="coldbox.system.web.services.BaseService" accessors="true" { // process context discovery of incoming pattern var contextRoute = findRoute( argumentCollection = contextRouting ); - // Return if route Not found. - if ( !contextRoute.route.isEmpty() ) { - return contextRoute; - } + // A module or namespace mount point is not itself an executable route. + // Return the nested result even when it is empty so a failed domain or + // condition match cannot fall back to the mount point. + return contextRoute; } // Save current routed details in PRC diff --git a/tests/specs/web/routing/RoutingServiceTest.cfc b/tests/specs/web/routing/RoutingServiceTest.cfc index 77a3f89d8..5270248cc 100755 --- a/tests/specs/web/routing/RoutingServiceTest.cfc +++ b/tests/specs/web/routing/RoutingServiceTest.cfc @@ -323,7 +323,7 @@ ); expect( allowed.route.event ).toBe( "Passkeys.authenticate" ); - expect( denied.route.event ).notToBe( "Passkeys.authenticate" ); + expect( denied.route ).toBeEmpty(); } finally { router.removeModuleRoutes( moduleName ); structDelete( modules, moduleName ); From 53716678b4b0df50c7f986de6f738687f6b6074b Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 28 Aug 2026 11:19:21 -0600 Subject: [PATCH 3/5] fix: recognize explicit module convention routes --- system/web/services/ModuleService.cfc | 2 +- tests/specs/integration/ModuleSpec.cfc | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/system/web/services/ModuleService.cfc b/system/web/services/ModuleService.cfc index 88aa0b51a..30c3a3a21 100755 --- a/system/web/services/ModuleService.cfc +++ b/system/web/services/ModuleService.cfc @@ -843,7 +843,7 @@ component extends="coldbox.system.web.services.BaseService" accessors="true" { var conventionsRouteExists = mConfig.router .getRoutes() .findAll( ( item ) => { - return ( item.pattern == "/:handler/:action" || item.pattern == ":handler/:action" ) + return reFindNoCase( "^/?\:handler/\:action\??/?$", item.pattern ) } ) if ( arrayLen( conventionsRouteExists ) == 0 ) { mConfig.router.route( "/:handler/:action?" ).end() diff --git a/tests/specs/integration/ModuleSpec.cfc b/tests/specs/integration/ModuleSpec.cfc index 117308e44..704c7f40c 100644 --- a/tests/specs/integration/ModuleSpec.cfc +++ b/tests/specs/integration/ModuleSpec.cfc @@ -111,6 +111,16 @@ component extends="tests.resources.BaseIntegrationTest" { expect( routing ).notToBeEmpty(); } ); + + then( "an explicit convention route should not be registered twice", () => { + var routingService = getController().getRoutingService(); + var routing = routingService.getModuleRoutes( "resourcesTest" ); + var conventionRoutes = routing.filter( ( item ) => { + return reFindNoCase( "^/?\:handler/\:action\??/?$", item.pattern ); + } ); + + expect( conventionRoutes ).toHaveLength( 1 ); + } ); } ); } ); From 85204cbac430dfe940eca6c10de6779499bec69e Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 28 Aug 2026 11:23:57 -0600 Subject: [PATCH 4/5] fix: continue routing after unmatched module routes --- system/web/services/RoutingService.cfc | 19 ++++++++++++++++--- .../specs/web/routing/RoutingServiceTest.cfc | 18 ++++++++++++------ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/system/web/services/RoutingService.cfc b/system/web/services/RoutingService.cfc index f51a70533..203316aaa 100644 --- a/system/web/services/RoutingService.cfc +++ b/system/web/services/RoutingService.cfc @@ -660,10 +660,23 @@ component extends="coldbox.system.web.services.BaseService" accessors="true" { // process context discovery of incoming pattern var contextRoute = findRoute( argumentCollection = contextRouting ); + if ( !contextRoute.route.isEmpty() ) { + return contextRoute; + } + // A module or namespace mount point is not itself an executable route. - // Return the nested result even when it is empty so a failed domain or - // condition match cannot fall back to the mount point. - return contextRoute; + // When its nested table declines the request, continue through the outer + // table so application fallbacks can handle unmatched domains or conditions. + var nextExcludedPatterns = duplicate( arguments.excludedPatterns ); + nextExcludedPatterns.append( results.route.pattern ); + return findRoute( + action = arguments.action, + domain = arguments.domain, + event = arguments.event, + module = arguments.module, + namespace = arguments.namespace, + excludedPatterns = nextExcludedPatterns + ); } // Save current routed details in PRC diff --git a/tests/specs/web/routing/RoutingServiceTest.cfc b/tests/specs/web/routing/RoutingServiceTest.cfc index 5270248cc..2f7884b0c 100755 --- a/tests/specs/web/routing/RoutingServiceTest.cfc +++ b/tests/specs/web/routing/RoutingServiceTest.cfc @@ -283,10 +283,11 @@ } ); it( "preserves the request domain when resolving module routes", function(){ - var moduleName = "domainRoutingTest"; - var router = getController().getRoutingService().getRouter(); - var modules = getController().getSetting( "modules" ); - var mockEvent = createMock( "coldbox.system.web.context.RequestContext" ).init( + var moduleName = "domainRoutingTest"; + var router = getController().getRoutingService().getRouter(); + var modules = getController().getSetting( "modules" ); + var originalRoutes = duplicate( router.getRoutes() ); + var mockEvent = createMock( "coldbox.system.web.context.RequestContext" ).init( controller = getController(), properties = { defaultLayout : "Main.cfm", @@ -299,6 +300,11 @@ modules[ moduleName ] = { resources : [], routes : [] }; try { + router.addRoute( + pattern = "/domain-module/ceremony", + event = "Fallback.notFound", + append = false + ); router.addModuleRoutes( pattern = "/domain-module", module = moduleName, @@ -310,7 +316,6 @@ domain = "allowed.example", module = moduleName ); - var allowed = routingService.findRoute( action = "/domain-module/ceremony", domain = "allowed.example", @@ -323,9 +328,10 @@ ); expect( allowed.route.event ).toBe( "Passkeys.authenticate" ); - expect( denied.route ).toBeEmpty(); + expect( denied.route.event ).toBe( "Fallback.notFound" ); } finally { router.removeModuleRoutes( moduleName ); + router.setRoutes( originalRoutes ); structDelete( modules, moduleName ); } } ); From 1241706a5ad06319dce8ca56be1a8db58b0c47a3 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 28 Aug 2026 11:29:43 -0600 Subject: [PATCH 5/5] fix: stop at unmatched nested route mounts --- system/web/services/RoutingService.cfc | 15 +++------------ tests/specs/web/routing/RoutingServiceTest.cfc | 17 +++++------------ 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/system/web/services/RoutingService.cfc b/system/web/services/RoutingService.cfc index 203316aaa..7450a97f2 100644 --- a/system/web/services/RoutingService.cfc +++ b/system/web/services/RoutingService.cfc @@ -665,18 +665,9 @@ component extends="coldbox.system.web.services.BaseService" accessors="true" { } // A module or namespace mount point is not itself an executable route. - // When its nested table declines the request, continue through the outer - // table so application fallbacks can handle unmatched domains or conditions. - var nextExcludedPatterns = duplicate( arguments.excludedPatterns ); - nextExcludedPatterns.append( results.route.pattern ); - return findRoute( - action = arguments.action, - domain = arguments.domain, - event = arguments.event, - module = arguments.module, - namespace = arguments.namespace, - excludedPatterns = nextExcludedPatterns - ); + // Return the empty nested result so a failed domain or condition match + // cannot fall back to the mount point. + return contextRoute; } // Save current routed details in PRC diff --git a/tests/specs/web/routing/RoutingServiceTest.cfc b/tests/specs/web/routing/RoutingServiceTest.cfc index 2f7884b0c..d6a281ad5 100755 --- a/tests/specs/web/routing/RoutingServiceTest.cfc +++ b/tests/specs/web/routing/RoutingServiceTest.cfc @@ -283,11 +283,10 @@ } ); it( "preserves the request domain when resolving module routes", function(){ - var moduleName = "domainRoutingTest"; - var router = getController().getRoutingService().getRouter(); - var modules = getController().getSetting( "modules" ); - var originalRoutes = duplicate( router.getRoutes() ); - var mockEvent = createMock( "coldbox.system.web.context.RequestContext" ).init( + var moduleName = "domainRoutingTest"; + var router = getController().getRoutingService().getRouter(); + var modules = getController().getSetting( "modules" ); + var mockEvent = createMock( "coldbox.system.web.context.RequestContext" ).init( controller = getController(), properties = { defaultLayout : "Main.cfm", @@ -300,11 +299,6 @@ modules[ moduleName ] = { resources : [], routes : [] }; try { - router.addRoute( - pattern = "/domain-module/ceremony", - event = "Fallback.notFound", - append = false - ); router.addModuleRoutes( pattern = "/domain-module", module = moduleName, @@ -328,10 +322,9 @@ ); expect( allowed.route.event ).toBe( "Passkeys.authenticate" ); - expect( denied.route.event ).toBe( "Fallback.notFound" ); + expect( denied.route ).toBeEmpty(); } finally { router.removeModuleRoutes( moduleName ); - router.setRoutes( originalRoutes ); structDelete( modules, moduleName ); } } );