diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go index 74cca4a4f..47448f925 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -464,6 +464,7 @@ func (ServerType) extractNamedRoutes( replacer ShorthandReplacer, ) ([]serverBlock, error) { namedRoutes := map[string]*caddyhttp.Route{} + namedRouteInvokes := map[string][]string{} gc := counter{new(int)} state := make(map[string]any) @@ -528,8 +529,19 @@ func (ServerType) extractNamedRoutes( return nil, fmt.Errorf("cannot have duplicate named_routes: %s", key) } namedRoutes[key] = &route + + // remember which named routes this named route invokes, + // so they can also be attached to any server that invokes + // this one + if state[namedRouteKey] != nil { + for name := range state[namedRouteKey].(map[string]struct{}) { + namedRouteInvokes[key] = append(namedRouteInvokes[key], name) + } + state[namedRouteKey] = nil + } } options["named_routes"] = namedRoutes + options["named_route_invokes"] = namedRouteInvokes return filtered, nil } @@ -721,6 +733,7 @@ func (st *ServerType) serversFromPairings( // add named routes to the server if 'invoke' was used inside of it configuredNamedRoutes := options["named_routes"].(map[string]*caddyhttp.Route) + namedRouteInvokes := options["named_route_invokes"].(map[string][]string) for _, sblock := range p.serverBlocks { if len(sblock.pile[namedRouteKey]) == 0 { continue @@ -729,11 +742,21 @@ func (st *ServerType) serversFromPairings( if srv.NamedRoutes == nil { srv.NamedRoutes = map[string]*caddyhttp.Route{} } - name := value.Value.(string) - if configuredNamedRoutes[name] == nil { - return nil, fmt.Errorf("cannot invoke named route '%s', which was not defined", name) + // named routes may invoke other named routes, so + // resolve the set of invoked names transitively + names := []string{value.Value.(string)} + for len(names) > 0 { + name := names[0] + names = names[1:] + if _, ok := srv.NamedRoutes[name]; ok { + continue + } + if configuredNamedRoutes[name] == nil { + return nil, fmt.Errorf("cannot invoke named route '%s', which was not defined", name) + } + srv.NamedRoutes[name] = configuredNamedRoutes[name] + names = append(names, namedRouteInvokes[name]...) } - srv.NamedRoutes[name] = configuredNamedRoutes[name] } } diff --git a/caddytest/integration/caddyfile_adapt/invoke_nested_named_routes.caddyfiletest b/caddytest/integration/caddyfile_adapt/invoke_nested_named_routes.caddyfiletest new file mode 100644 index 000000000..937723d3c --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/invoke_nested_named_routes.caddyfiletest @@ -0,0 +1,114 @@ +&(a) { + invoke b + respond "a" +} + +&(b) { + invoke c + respond "b" +} + +&(c) { + respond "c" +} + +&(loop1) { + invoke loop2 + respond "loop1" +} + +&(loop2) { + invoke loop1 + respond "loop2" +} + +:8080 { + invoke a +} + +:8081 { + invoke loop1 +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":8080" + ], + "routes": [ + { + "handle": [ + { + "handler": "invoke", + "name": "a" + } + ] + } + ], + "named_routes": { + "a": { + "handle": [ + { + "handler": "invoke", + "name": "b" + } + ] + }, + "b": { + "handle": [ + { + "handler": "invoke", + "name": "c" + } + ] + }, + "c": { + "handle": [ + { + "body": "c", + "handler": "static_response" + } + ] + } + } + }, + "srv1": { + "listen": [ + ":8081" + ], + "routes": [ + { + "handle": [ + { + "handler": "invoke", + "name": "loop1" + } + ] + } + ], + "named_routes": { + "loop1": { + "handle": [ + { + "handler": "invoke", + "name": "loop2" + } + ] + }, + "loop2": { + "handle": [ + { + "handler": "invoke", + "name": "loop1" + } + ] + } + } + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/invoke_nested_undefined_named_route.caddyfiletest b/caddytest/integration/caddyfile_adapt/invoke_nested_undefined_named_route.caddyfiletest new file mode 100644 index 000000000..a3feca7f1 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/invoke_nested_undefined_named_route.caddyfiletest @@ -0,0 +1,10 @@ +&(outer) { + invoke missing + respond "outer" +} + +example.com { + invoke outer +} +---------- +cannot invoke named route 'missing', which was not defined