Skip to content

Commit 5850937

Browse files
fix(policy-engine): resolve upstream base path for dynamic routing
Two bugs prevented the dynamic-endpoint policy from applying a target upstream definition's base path when routing via UpstreamName: - upstreamDefinitionPaths was looked up by the raw definition name, but the map is keyed by the full cluster key; the lookup always missed. Look it up by the constructed cluster name instead. - the routed path was built by naive concatenation, leaving the API context in place (e.g. /foo/ctx/v1/items). Use computeUpstreamPath to strip the context before prepending the upstream base path. Add gateway integration test features/dynamic-endpoint.feature covering named-upstream routing, per-operation base paths, and the required targetUpstream parameter, and register it in the default suite.
1 parent bf0e4ea commit 5850937

4 files changed

Lines changed: 178 additions & 6 deletions

File tree

gateway/gateway-runtime/policy-engine/internal/kernel/extproc.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,7 @@ type RouteMetadata struct {
455455
ProjectID string
456456
DefaultUpstreamCluster string // Default cluster for dynamic cluster routing
457457
UpstreamBasePath string // Base path for the upstream (e.g., /anything)
458-
UpstreamDefinitionPaths map[string]string // Maps upstream definition names to their URL paths
458+
UpstreamDefinitionPaths map[string]string // Maps upstream definition cluster keys to their URL base paths
459459
}
460460

461461
// generateRequestID generates a unique request identifier

gateway/gateway-runtime/policy-engine/internal/kernel/translator.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ func translateRequestActionsCore(result *executor.RequestExecutionResult, execCt
270270
"upstreamDefPaths", execCtx.upstreamDefinitionPaths,
271271
"apiContext", execCtx.apiContext)
272272
if execCtx.upstreamDefinitionPaths != nil {
273-
if targetUpstreamPath, ok := execCtx.upstreamDefinitionPaths[*targetUpstreamName]; ok {
273+
if targetUpstreamPath, ok := execCtx.upstreamDefinitionPaths[clusterName]; ok {
274274
// Set in both local DynamicMetadata (for response to Envoy) and execCtx (for response phase)
275275
out.DynamicMetadata[extProcNS]["target_upstream_base_path"] = targetUpstreamPath
276276
execCtx.dynamicMetadata[extProcNS]["target_upstream_base_path"] = targetUpstreamPath
@@ -460,9 +460,9 @@ func TranslateRequestHeaderActions(result *executor.RequestHeaderExecutionResult
460460
"upstream_base_path": execCtx.upstreamBasePath,
461461
}
462462
if execCtx.upstreamDefinitionPaths != nil {
463-
if targetUpstreamPath, ok := execCtx.upstreamDefinitionPaths[*targetUpstreamName]; ok {
463+
if targetUpstreamPath, ok := execCtx.upstreamDefinitionPaths[clusterName]; ok {
464464
if mutations.Path == nil {
465-
computedPath := strings.TrimSuffix(targetUpstreamPath, "/") + execCtx.requestBodyCtx.Path
465+
computedPath := computeUpstreamPath(execCtx.requestBodyCtx.Path, execCtx.apiContext, targetUpstreamPath)
466466
mutations.Path = &computedPath
467467
dynamicMetadata[extProcNS]["request_transformation.target_path"] = computedPath
468468
execCtx.dynamicMetadata[extProcNS]["request_transformation.target_path"] = computedPath
@@ -684,9 +684,9 @@ func TranslateRequestHeaderActionsWithBodyMerge(
684684
dynamicMetadata[extProcNS]["api_context"] = execCtx.apiContext
685685
dynamicMetadata[extProcNS]["upstream_base_path"] = execCtx.upstreamBasePath
686686
if execCtx.upstreamDefinitionPaths != nil {
687-
if targetUpstreamPath, ok := execCtx.upstreamDefinitionPaths[*targetUpstreamName]; ok {
687+
if targetUpstreamPath, ok := execCtx.upstreamDefinitionPaths[clusterName]; ok {
688688
if mutations.Path == nil {
689-
computedPath := strings.TrimSuffix(targetUpstreamPath, "/") + execCtx.requestBodyCtx.Path
689+
computedPath := computeUpstreamPath(execCtx.requestBodyCtx.Path, execCtx.apiContext, targetUpstreamPath)
690690
mutations.Path = &computedPath
691691
dynamicMetadata[extProcNS]["request_transformation.target_path"] = computedPath
692692
execCtx.dynamicMetadata[extProcNS]["request_transformation.target_path"] = computedPath
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# --------------------------------------------------------------------
2+
# Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
3+
#
4+
# WSO2 LLC. licenses this file to you under the Apache License,
5+
# Version 2.0 (the "License"); you may not use this file except
6+
# in compliance with the License. You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing,
11+
# software distributed under the License is distributed on an
12+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
13+
# KIND, either express or implied. See the License for the
14+
# specific language governing permissions and limitations
15+
# under the License.
16+
# --------------------------------------------------------------------
17+
18+
@dynamic-endpoint
19+
Feature: Dynamic Endpoint policy
20+
As an API developer
21+
I want the dynamic-endpoint policy to route an operation to a named upstream definition
22+
So that specific operations can target alternate upstreams without changing the primary API structure
23+
24+
Background:
25+
Given the gateway services are running
26+
27+
# The policy sets the SDK UpstreamName field, diverting the request from the default
28+
# upstream.main to the named upstream definition. Operations without the policy keep
29+
# using upstream.main.
30+
Scenario: Operation routed to a named upstream definition while others use the default upstream
31+
Given I authenticate using basic auth as "admin"
32+
When I deploy this API configuration:
33+
"""
34+
apiVersion: gateway.api-platform.wso2.com/v1alpha1
35+
kind: RestApi
36+
metadata:
37+
name: dynamic-endpoint-api-v1.0
38+
spec:
39+
displayName: Dynamic-Endpoint-API
40+
version: v1.0
41+
context: /dynamic-endpoint/$version
42+
upstreamDefinitions:
43+
- name: alt-upstream
44+
upstreams:
45+
- url: http://sample-backend:9080/alternate
46+
upstream:
47+
main:
48+
url: http://sample-backend:9080
49+
operations:
50+
- method: GET
51+
path: /whoami
52+
policies:
53+
- name: dynamic-endpoint
54+
version: v1
55+
params:
56+
targetUpstream: alt-upstream
57+
- method: GET
58+
path: /ping
59+
"""
60+
Then the response should be successful
61+
And I wait for the endpoint "http://localhost:8080/dynamic-endpoint/v1.0/ping" to be ready
62+
63+
# Operation with the policy: diverted to alt-upstream. The backend echoes the
64+
# path it received — the alt-upstream base path /alternate confirms the routing.
65+
When I clear all headers
66+
And I send a GET request to "http://localhost:8080/dynamic-endpoint/v1.0/whoami"
67+
Then the response should be successful
68+
And the response should be valid JSON
69+
And the JSON response field "path" should be "/alternate/whoami"
70+
71+
# Operation without the policy: served by the default upstream.main (base path /).
72+
When I clear all headers
73+
And I send a GET request to "http://localhost:8080/dynamic-endpoint/v1.0/ping"
74+
Then the response should be successful
75+
And the response should be valid JSON
76+
And the JSON response field "path" should be "/ping"
77+
78+
Given I authenticate using basic auth as "admin"
79+
When I delete the API "dynamic-endpoint-api-v1.0"
80+
Then the response should be successful
81+
82+
# Each upstream definition carries a distinct base path. The policy must route each
83+
# operation to its targeted upstream AND the upstream's base path must be prepended
84+
# to the forwarded request path.
85+
Scenario: Different operations route to upstreams with different base paths
86+
Given I authenticate using basic auth as "admin"
87+
When I deploy this API configuration:
88+
"""
89+
apiVersion: gateway.api-platform.wso2.com/v1alpha1
90+
kind: RestApi
91+
metadata:
92+
name: dynamic-endpoint-routes-v1.0
93+
spec:
94+
displayName: Dynamic-Endpoint-Routes-API
95+
version: v1.0
96+
context: /dynamic-endpoint-routes/$version
97+
upstreamDefinitions:
98+
- name: foo-upstream
99+
upstreams:
100+
- url: http://sample-backend:9080/foo
101+
- name: bar-upstream
102+
upstreams:
103+
- url: http://sample-backend:9080/bar
104+
upstream:
105+
main:
106+
url: http://sample-backend:9080
107+
operations:
108+
- method: GET
109+
path: /items
110+
policies:
111+
- name: dynamic-endpoint
112+
version: v1
113+
params:
114+
targetUpstream: foo-upstream
115+
- method: GET
116+
path: /records
117+
policies:
118+
- name: dynamic-endpoint
119+
version: v1
120+
params:
121+
targetUpstream: bar-upstream
122+
"""
123+
Then the response should be successful
124+
And I wait for the endpoint "http://localhost:8080/dynamic-endpoint-routes/v1.0/items" to be ready
125+
126+
# Routed to foo-upstream: base path /foo prepended.
127+
When I clear all headers
128+
And I send a GET request to "http://localhost:8080/dynamic-endpoint-routes/v1.0/items"
129+
Then the response should be successful
130+
And the response should be valid JSON
131+
And the JSON response field "path" should be "/foo/items"
132+
133+
# Routed to bar-upstream: base path /bar prepended.
134+
When I clear all headers
135+
And I send a GET request to "http://localhost:8080/dynamic-endpoint-routes/v1.0/records"
136+
Then the response should be successful
137+
And the response should be valid JSON
138+
And the JSON response field "path" should be "/bar/records"
139+
140+
Given I authenticate using basic auth as "admin"
141+
When I delete the API "dynamic-endpoint-routes-v1.0"
142+
Then the response should be successful
143+
144+
# targetUpstream is a required parameter in the policy definition.
145+
Scenario: Deploy fails when targetUpstream is omitted
146+
Given I authenticate using basic auth as "admin"
147+
When I deploy this API configuration:
148+
"""
149+
apiVersion: gateway.api-platform.wso2.com/v1alpha1
150+
kind: RestApi
151+
metadata:
152+
name: dynamic-endpoint-missing-param-v1.0
153+
spec:
154+
displayName: Dynamic-Endpoint-Missing-Param-API
155+
version: v1.0
156+
context: /dynamic-endpoint-missing/$version
157+
upstream:
158+
main:
159+
url: http://sample-backend:9080
160+
operations:
161+
- method: GET
162+
path: /whoami
163+
policies:
164+
- name: dynamic-endpoint
165+
version: v1
166+
params: {}
167+
"""
168+
Then the response should be a client error
169+
And the response should be valid JSON
170+
And the JSON response field "status" should be "error"
171+
And the response body should contain "targetUpstream"

gateway/it/suite_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ func getFeaturePaths() []string {
134134
"features/analytics-basic.feature",
135135
"features/token-based-ratelimit.feature",
136136
"features/sandbox-routing.feature",
137+
"features/dynamic-endpoint.feature",
137138
"features/subscription-validation.feature",
138139
"features/subscription-analytics.feature",
139140
"features/llm-cost-based-ratelimit.feature",

0 commit comments

Comments
 (0)