Skip to content

Commit 3f1f3f0

Browse files
committed
feat: add Body scope to ApisixRoute match expressions for request body matching
Add ScopeBody to ApisixRouteHTTPMatchExprSubject.Scope, which maps to APISIX's post_arg.* variable. This supports request body matching for application/json, application/x-www-form-urlencoded, and multipart/form-data content types, and allows dot-notation JSON path expressions such as 'model.version' and 'messages[*].role'. Closes #399
1 parent 44b9754 commit 3f1f3f0

3 files changed

Lines changed: 100 additions & 2 deletions

File tree

api/v2/apisixroute_types.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -310,8 +310,10 @@ func (exprs ApisixRouteHTTPMatchExprs) ToVars() (result adc.Vars, err error) {
310310
subj = "uri"
311311
case ScopeVariable:
312312
subj = expr.Subject.Name
313+
case ScopeBody:
314+
subj = "post_arg." + expr.Subject.Name
313315
default:
314-
return result, errors.New("invalid http match expr: subject.scope should be one of [query, header, cookie, path, variable]")
316+
return result, errors.New("invalid http match expr: subject.scope should be one of [query, header, cookie, path, variable, body]")
315317
}
316318
this.SliceVal = append(this.SliceVal, adc.StringOrSlice{StrVal: subj})
317319

@@ -411,8 +413,12 @@ type ApisixRouteAuthenticationLDAPAuth struct {
411413

412414
// ApisixRouteHTTPMatchExprSubject describes the subject of a route matching expression.
413415
type ApisixRouteHTTPMatchExprSubject struct {
414-
// Scope specifies the subject scope and can be `Header`, `Query`, or `Path`.
416+
// Scope specifies the subject scope.
417+
// Supported values: `Header`, `Query`, `Path`, `Cookie`, `Variable`, `Body`.
415418
// When Scope is `Path`, Name will be ignored.
419+
// When Scope is `Body`, Name supports dot-notation JSON path (e.g., "model.version",
420+
// "messages[*].role") and maps to APISIX's post_arg.* variable, which works with
421+
// application/json, application/x-www-form-urlencoded, and multipart/form-data.
416422
Scope string `json:"scope" yaml:"scope"`
417423
// Name is the name of the header or query parameter.
418424
Name string `json:"name" yaml:"name"`

api/v2/apisixroute_types_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package v2
19+
20+
import (
21+
"testing"
22+
23+
"github.com/stretchr/testify/assert"
24+
"github.com/stretchr/testify/require"
25+
)
26+
27+
func strPtr(s string) *string { return &s }
28+
29+
func TestToVars_ScopeBody_SimpleField(t *testing.T) {
30+
exprs := ApisixRouteHTTPMatchExprs{
31+
{
32+
Subject: ApisixRouteHTTPMatchExprSubject{
33+
Scope: ScopeBody,
34+
Name: "action",
35+
},
36+
Op: OpEqual,
37+
Value: strPtr("login"),
38+
},
39+
}
40+
41+
vars, err := exprs.ToVars()
42+
require.NoError(t, err)
43+
require.Len(t, vars, 1)
44+
45+
// vars[0] is []StringOrSlice: [subject, op, value]
46+
// Should map to post_arg.action
47+
assert.Equal(t, "post_arg.action", vars[0][0].StrVal)
48+
assert.Equal(t, "==", vars[0][1].StrVal)
49+
assert.Equal(t, "login", vars[0][2].StrVal)
50+
}
51+
52+
func TestToVars_ScopeBody_NestedJSONPath(t *testing.T) {
53+
exprs := ApisixRouteHTTPMatchExprs{
54+
{
55+
Subject: ApisixRouteHTTPMatchExprSubject{
56+
Scope: ScopeBody,
57+
Name: "model.version",
58+
},
59+
Op: OpEqual,
60+
Value: strPtr("gpt-4"),
61+
},
62+
}
63+
64+
vars, err := exprs.ToVars()
65+
require.NoError(t, err)
66+
require.Len(t, vars, 1)
67+
68+
// Should map to post_arg.model.version (dot-notation passthrough)
69+
assert.Equal(t, "post_arg.model.version", vars[0][0].StrVal)
70+
}
71+
72+
func TestToVars_ScopeBody_EmptyName_ReturnsError(t *testing.T) {
73+
exprs := ApisixRouteHTTPMatchExprs{
74+
{
75+
Subject: ApisixRouteHTTPMatchExprSubject{
76+
Scope: ScopeBody,
77+
Name: "",
78+
},
79+
Op: OpEqual,
80+
Value: strPtr("login"),
81+
},
82+
}
83+
84+
_, err := exprs.ToVars()
85+
assert.Error(t, err)
86+
assert.Contains(t, err.Error(), "empty subject.name")
87+
}

api/v2/shared_types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ const (
8686
ScopeCookie = "Cookie"
8787
// ScopeVariable means the route match expression subject is in variable.
8888
ScopeVariable = "Variable"
89+
// ScopeBody means the route match expression subject is in the request body.
90+
// Name supports dot-notation JSON path (e.g., "model.version", "messages[*].role"),
91+
// and maps to APISIX's post_arg.* variable, which supports application/json,
92+
// application/x-www-form-urlencoded, and multipart/form-data content types.
93+
ScopeBody = "Body"
8994
)
9095

9196
const (

0 commit comments

Comments
 (0)