Skip to content

Commit 86a65b5

Browse files
Merge branch 'release-5.12.1' into TT-16932-5.12.1
2 parents 22c6222 + b6d06a7 commit 86a65b5

6 files changed

Lines changed: 658 additions & 32 deletions

File tree

.github/workflows/plugin-compiler-build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ on:
1111
- "v*"
1212

1313
env:
14-
GOLANG_CROSS: 1.24-bullseye
14+
GOLANG_CROSS: 1.25-bullseye
1515

1616
concurrency:
1717
group: ${{ github.workflow }}-${{ github.ref }}

ci/images/plugin-compiler/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
ARG BASE_IMAGE=tykio/golang-cross:1.24-bullseye
1+
ARG BASE_IMAGE=tykio/golang-cross:1.25-bullseye
22
FROM ${BASE_IMAGE}
33

44
LABEL description="Image for plugin development"

gateway/api_definition.go

Lines changed: 85 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"net/url"
1313
"os"
1414
"path/filepath"
15+
"sort"
1516
"strings"
1617
"sync/atomic"
1718
texttemplate "text/template"
@@ -23,6 +24,7 @@ import (
2324
"github.com/TykTechnologies/tyk/internal/httpctx"
2425
"github.com/TykTechnologies/tyk/internal/httputil"
2526
"github.com/TykTechnologies/tyk/internal/mcp"
27+
"github.com/TykTechnologies/tyk/internal/oasutil"
2628

2729
"github.com/getkin/kin-openapi/routers/gorillamux"
2830

@@ -1372,7 +1374,6 @@ func (a APIDefinitionLoader) compileOASValidateRequestPathSpec(apiSpec *APISpec,
13721374
continue
13731375
}
13741376

1375-
// Find the path and method for this operation
13761377
path, method := a.findPathAndMethodForOperation(apiSpec, operationID)
13771378
if path == "" || method == "" {
13781379
continue
@@ -1384,14 +1385,20 @@ func (a APIDefinitionLoader) compileOASValidateRequestPathSpec(apiSpec *APISpec,
13841385
OASPath: path,
13851386
}
13861387

1387-
// The path in OAS is relative to the server URL (listenPath)
1388-
// For regex matching, we don't prepend listenPath because URLSpec.matchesPath
1389-
// will strip the listenPath before matching
1390-
// Use standard regex generation with gateway config
13911388
a.generateRegex(path, &newSpec, OASValidateRequest, conf)
13921389
urlSpec = append(urlSpec, newSpec)
13931390
}
13941391

1392+
urlSpec = a.addStaticPathShields(apiSpec, conf, urlSpec, OASValidateRequest, func(path, method string) URLSpec {
1393+
return URLSpec{
1394+
OASValidateRequestMeta: &oas.ValidateRequest{Enabled: false},
1395+
OASMethod: method,
1396+
OASPath: path,
1397+
}
1398+
})
1399+
1400+
sortURLSpecsByPathPriority(urlSpec)
1401+
13951402
return urlSpec
13961403
}
13971404

@@ -1417,7 +1424,6 @@ func (a APIDefinitionLoader) compileOASMockResponsePathSpec(apiSpec *APISpec, co
14171424
continue
14181425
}
14191426

1420-
// Find the path and method for this operation
14211427
path, method := a.findPathAndMethodForOperation(apiSpec, operationID)
14221428
if path == "" || method == "" {
14231429
continue
@@ -1429,14 +1435,78 @@ func (a APIDefinitionLoader) compileOASMockResponsePathSpec(apiSpec *APISpec, co
14291435
OASPath: path,
14301436
}
14311437

1432-
// Use standard regex generation with gateway config
14331438
a.generateRegex(path, &newSpec, OASMockResponse, conf)
14341439
urlSpec = append(urlSpec, newSpec)
14351440
}
14361441

1442+
urlSpec = a.addStaticPathShields(apiSpec, conf, urlSpec, OASMockResponse, func(path, method string) URLSpec {
1443+
return URLSpec{
1444+
OASMockResponseMeta: &oas.MockResponse{Enabled: false},
1445+
OASMethod: method,
1446+
OASPath: path,
1447+
}
1448+
})
1449+
1450+
sortURLSpecsByPathPriority(urlSpec)
1451+
1452+
return urlSpec
1453+
}
1454+
1455+
// addStaticPathShields adds synthetic disabled URLSpec entries for static OAS paths
1456+
// that don't already have an entry in urlSpec. These entries act as shields: when the
1457+
// middleware scans the path list, a static shield entry matches before any parameterised
1458+
// regex, and the middleware sees Enabled=false and skips it. This prevents parameterised
1459+
// paths (e.g. /employees/{id}) from incorrectly matching static paths (e.g. /employees/static).
1460+
//
1461+
// Shield entries are only added when urlSpec contains at least one parameterised path,
1462+
// since without parameterised paths there is no cross-matching risk.
1463+
func (a APIDefinitionLoader) addStaticPathShields(
1464+
apiSpec *APISpec,
1465+
conf config.Config,
1466+
urlSpec []URLSpec,
1467+
status URLStatus,
1468+
newDisabledSpec func(path, method string) URLSpec,
1469+
) []URLSpec {
1470+
if apiSpec.OAS.Paths == nil {
1471+
return urlSpec
1472+
}
1473+
1474+
existing, hasParameterised := indexURLSpecs(urlSpec)
1475+
if !hasParameterised {
1476+
return urlSpec
1477+
}
1478+
1479+
for path, pathItem := range apiSpec.OAS.Paths.Map() {
1480+
if httputil.IsMuxTemplate(path) {
1481+
continue
1482+
}
1483+
for method := range pathItem.Operations() {
1484+
method = strings.ToUpper(method)
1485+
if _, exists := existing[path+":"+method]; exists {
1486+
continue
1487+
}
1488+
newSpec := newDisabledSpec(path, method)
1489+
a.generateRegex(path, &newSpec, status, conf)
1490+
urlSpec = append(urlSpec, newSpec)
1491+
}
1492+
}
1493+
14371494
return urlSpec
14381495
}
14391496

1497+
// indexURLSpecs builds a set of "path:METHOD" keys from the given specs and reports
1498+
// whether any spec uses a parameterised (mux-template) path.
1499+
func indexURLSpecs(specs []URLSpec) (existing map[string]struct{}, hasParameterised bool) {
1500+
existing = make(map[string]struct{}, len(specs))
1501+
for _, spec := range specs {
1502+
existing[spec.OASPath+":"+spec.OASMethod] = struct{}{}
1503+
if httputil.IsMuxTemplate(spec.OASPath) {
1504+
hasParameterised = true
1505+
}
1506+
}
1507+
return
1508+
}
1509+
14401510
// findPathAndMethodForOperation finds the path and method for a given operation ID
14411511
// by searching through the OAS paths.
14421512
func (a APIDefinitionLoader) findPathAndMethodForOperation(apiSpec *APISpec, operationID string) (string, string) {
@@ -1455,6 +1525,14 @@ func (a APIDefinitionLoader) findPathAndMethodForOperation(apiSpec *APISpec, ope
14551525
return "", ""
14561526
}
14571527

1528+
// sortURLSpecsByPathPriority sorts URLSpec entries using the same path priority
1529+
// rules as oasutil.SortByPathLength, ensuring consistent ordering across the gateway.
1530+
func sortURLSpecsByPathPriority(specs []URLSpec) {
1531+
sort.Slice(specs, func(i, j int) bool {
1532+
return oasutil.PathLess(specs[i].OASPath, specs[j].OASPath)
1533+
})
1534+
}
1535+
14581536
// extractMCPPrimitivesToPaths extracts MCP primitives (tools, resources, prompts) from the OAS
14591537
// definition and populates them into the ExtendedPaths structure for each API version.
14601538
// It also adds built-in MCP operation paths (tools/call, resources/read, prompts/get) to
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package gateway
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
9+
"github.com/getkin/kin-openapi/openapi3"
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/TykTechnologies/tyk/apidef/oas"
13+
)
14+
15+
// BenchmarkSortURLSpecsByPathPriority measures the overhead of sorting URLSpec entries.
16+
func BenchmarkSortURLSpecsByPathPriority(b *testing.B) {
17+
for _, n := range []int{10, 50, 200} {
18+
b.Run(fmt.Sprintf("paths=%d", n), func(b *testing.B) {
19+
// Build a mix of static and parameterised paths
20+
template := make([]URLSpec, n)
21+
for i := 0; i < n; i++ {
22+
if i%3 == 0 {
23+
template[i] = URLSpec{OASPath: fmt.Sprintf("/api/v1/resource%d/{id}", i)}
24+
} else {
25+
template[i] = URLSpec{OASPath: fmt.Sprintf("/api/v1/resource%d/static", i)}
26+
}
27+
}
28+
29+
b.ReportAllocs()
30+
b.ResetTimer()
31+
for i := 0; i < b.N; i++ {
32+
specs := make([]URLSpec, n)
33+
copy(specs, template)
34+
sortURLSpecsByPathPriority(specs)
35+
}
36+
})
37+
}
38+
}
39+
40+
// BenchmarkOASValidateRequestStaticVsParameterized measures request-time performance
41+
// for static and parameterised paths with the shield mechanism.
42+
func BenchmarkOASValidateRequestStaticVsParameterized(b *testing.B) {
43+
ts := StartTest(nil)
44+
defer ts.Close()
45+
46+
paths := openapi3.NewPaths()
47+
48+
// Parameterised path with validation
49+
paths.Set("/users/{id}", &openapi3.PathItem{
50+
Get: &openapi3.Operation{
51+
OperationID: "getUserById",
52+
Parameters: openapi3.Parameters{
53+
&openapi3.ParameterRef{
54+
Value: &openapi3.Parameter{
55+
Name: "id",
56+
In: "path",
57+
Required: true,
58+
Schema: &openapi3.SchemaRef{
59+
Value: &openapi3.Schema{
60+
Type: &openapi3.Types{"integer"},
61+
},
62+
},
63+
},
64+
},
65+
},
66+
Responses: openapi3.NewResponses(
67+
openapi3.WithStatus(200, &openapi3.ResponseRef{
68+
Value: &openapi3.Response{
69+
Description: stringPtrHelper("Success"),
70+
},
71+
}),
72+
),
73+
},
74+
})
75+
76+
// Static path without validation
77+
paths.Set("/users/admin", &openapi3.PathItem{
78+
Get: &openapi3.Operation{
79+
OperationID: "getAdminUser",
80+
Responses: openapi3.NewResponses(
81+
openapi3.WithStatus(200, &openapi3.ResponseRef{
82+
Value: &openapi3.Response{
83+
Description: stringPtrHelper("Success"),
84+
},
85+
}),
86+
),
87+
},
88+
})
89+
90+
// Add extra static paths to test scaling
91+
for i := 0; i < 50; i++ {
92+
opID := fmt.Sprintf("getResource%d", i)
93+
paths.Set(fmt.Sprintf("/resources/item%d", i), &openapi3.PathItem{
94+
Get: &openapi3.Operation{
95+
OperationID: opID,
96+
Responses: openapi3.NewResponses(
97+
openapi3.WithStatus(200, &openapi3.ResponseRef{
98+
Value: &openapi3.Response{
99+
Description: stringPtrHelper("Success"),
100+
},
101+
}),
102+
),
103+
},
104+
})
105+
}
106+
107+
doc := openapi3.T{
108+
OpenAPI: "3.0.0",
109+
Info: &openapi3.Info{Title: "Benchmark API", Version: "1.0.0"},
110+
Paths: paths,
111+
}
112+
113+
oasAPI := oas.OAS{T: doc}
114+
oasAPI.SetTykExtension(&oas.XTykAPIGateway{
115+
Middleware: &oas.Middleware{
116+
Operations: oas.Operations{
117+
"getUserById": {
118+
ValidateRequest: &oas.ValidateRequest{Enabled: true},
119+
},
120+
},
121+
},
122+
})
123+
124+
api := ts.Gw.BuildAndLoadAPI(func(spec *APISpec) {
125+
spec.Name = "Benchmark API"
126+
spec.APIID = "benchmark-api"
127+
spec.Proxy.ListenPath = "/api/"
128+
spec.UseKeylessAccess = true
129+
spec.IsOAS = true
130+
spec.OAS = oasAPI
131+
})[0]
132+
133+
require.NotNil(b, api)
134+
135+
b.Run("StaticPath_ShieldHit", func(b *testing.B) {
136+
req := httptest.NewRequest(http.MethodGet, "/api/users/admin", nil)
137+
b.ReportAllocs()
138+
b.ResetTimer()
139+
for i := 0; i < b.N; i++ {
140+
rec := httptest.NewRecorder()
141+
ts.TestServerRouter.ServeHTTP(rec, req)
142+
}
143+
})
144+
145+
b.Run("ParameterizedPath_ValidationHit", func(b *testing.B) {
146+
req := httptest.NewRequest(http.MethodGet, "/api/users/123", nil)
147+
b.ReportAllocs()
148+
b.ResetTimer()
149+
for i := 0; i < b.N; i++ {
150+
rec := httptest.NewRecorder()
151+
ts.TestServerRouter.ServeHTTP(rec, req)
152+
}
153+
})
154+
}

0 commit comments

Comments
 (0)