Skip to content

Commit 2136215

Browse files
fix: strip query parameters from request path before parser resolution (llm-d#1585)
* fix: strip query parameters from request path before parser resolution The HTTP/2 :path pseudo-header includes query parameters (e.g. /v1/messages?beta=true), which caused parser suffix matching and per-parser endpoint validation to fail for clients that append query strings. Strip query parameters at header ingestion in HandleRequestHeaders so all downstream consumers see a clean path. Signed-off-by: greg pereira <gpereira@redhat.com> Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: greg pereira <grpereir@redhat.com> * fix: add comment explaining query parameter stripping from :path Signed-off-by: greg pereira <grpereir@redhat.com> * make implementation non mutating Signed-off-by: greg pereira <grpereir@redhat.com> --------- Signed-off-by: greg pereira <gpereira@redhat.com> Signed-off-by: greg pereira <grpereir@redhat.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 866c741 commit 2136215

3 files changed

Lines changed: 26 additions & 18 deletions

File tree

pkg/epp/framework/common/request/headers.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,26 @@ func GetHeader(headers map[string]string, key string) string {
3333
}
3434

3535
// GetRequestPath extracts the request path from headers with fallback priority.
36+
// Query parameters are stripped because the path is used only for parser routing.
3637
func GetRequestPath(headers map[string]string) string {
3738
if path := headers[":path"]; path != "" {
38-
return path
39+
return stripQuery(path)
3940
}
4041
if path := headers["x-original-path"]; path != "" {
41-
return path
42+
return stripQuery(path)
4243
}
4344
if path := headers["x-forwarded-path"]; path != "" {
44-
return path
45+
return stripQuery(path)
4546
}
4647
// Default to completions API for backward compatibility with existing clients and integration tests
4748
return "/v1/completions"
4849
}
4950

51+
func stripQuery(path string) string {
52+
clean, _, _ := strings.Cut(path, "?")
53+
return clean
54+
}
55+
5056
// MatchPathSuffix checks if the path matches the suffix.
5157
func MatchPathSuffix(path, suffix string) bool {
5258
path = strings.TrimSuffix(strings.TrimSpace(path), "/")

pkg/epp/framework/common/request/headers_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,21 @@ func TestGetRequestPath(t *testing.T) {
5151
headers: map[string]string{"x-forwarded-path": "/baz"},
5252
want: "/baz",
5353
},
54+
{
55+
name: "query parameters stripped from :path",
56+
headers: map[string]string{":path": "/v1/messages?beta=true"},
57+
want: "/v1/messages",
58+
},
59+
{
60+
name: "query parameters stripped from x-original-path",
61+
headers: map[string]string{"x-original-path": "/v1/messages?foo=bar&baz=1"},
62+
want: "/v1/messages",
63+
},
64+
{
65+
name: "query parameters stripped from x-forwarded-path",
66+
headers: map[string]string{"x-forwarded-path": "/v1/chat/completions?stream=true"},
67+
want: "/v1/chat/completions",
68+
},
5469
{
5570
name: "fallback to completions",
5671
headers: map[string]string{},

pkg/epp/framework/plugins/requesthandling/parsers/anthropic/anthropic.go

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ import (
2525

2626
v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
2727

28+
"github.com/llm-d/llm-d-router/pkg/epp/framework/common/request"
2829
fwkplugin "github.com/llm-d/llm-d-router/pkg/epp/framework/interface/plugin"
2930
fwkrh "github.com/llm-d/llm-d-router/pkg/epp/framework/interface/requesthandling"
30-
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/requesthandling/parsers"
3131
)
3232

3333
const (
@@ -79,7 +79,7 @@ func (p *AnthropicParser) WithName(name string) *AnthropicParser {
7979
}
8080

8181
func (p *AnthropicParser) ParseRequest(_ context.Context, body []byte, headers map[string]string) (*fwkrh.ParseResult, error) {
82-
path := getRequestPath(headers)
82+
path := request.GetRequestPath(headers)
8383

8484
// The count_tokens endpoint returns only a token count and gains nothing from
8585
// structured parsing or response interception; forward the body unchanged.
@@ -141,19 +141,6 @@ func (p *AnthropicParser) ParseResponse(_ context.Context, body []byte, headers
141141
return &fwkrh.ParsedResponse{Usage: usage}, nil
142142
}
143143

144-
func getRequestPath(headers map[string]string) string {
145-
if path := headers[parsers.MethodPathKey]; path != "" {
146-
return path
147-
}
148-
if path := headers["x-original-path"]; path != "" {
149-
return path
150-
}
151-
if path := headers["x-forwarded-path"]; path != "" {
152-
return path
153-
}
154-
return ""
155-
}
156-
157144
func extractUsage(responseBytes []byte) (*fwkrh.Usage, error) {
158145
var responseBody map[string]any
159146
if err := json.Unmarshal(responseBytes, &responseBody); err != nil {

0 commit comments

Comments
 (0)