feat(mcp-gateway): introduce ordered 5-stage request filter pipeline#299
Conversation
Replaces the monolithic handleGateway with a Filter/Exchange/Result pipeline (inspect → policy → auth → authz → upstream). Stage 6 audit runs unconditionally from the orchestrator, centralising all emit paths. errPolicyUnavailable sentinel added so authzFilter fails closed with 503 when no valid policy has been loaded. Adds per-filter unit tests and ordering proofs in pipeline_test.go: Continue/Reject/Respond per stage, auth-before-authz ordering assertion, authorization-input immutability test, and 503 on policy unavailable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request refactors the monolithic handleGateway function into a modular, six-stage request filter pipeline, introducing an Exchange struct to manage request-scoped state and separate files for each filter stage. It also adds comprehensive unit tests to verify pipeline execution and ordering guarantees. Feedback on these changes highlights a high-severity routing inconsistency in upstreamFilter where RawPath is left untrimmed if prefix trimming fails on it but succeeds on Path. Additionally, it is recommended to remove the redundant context.Context parameter from Filter.Handle since the context can be retrieved directly from the request inside the Exchange struct.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if trimmedRaw, rawOK := trimRequestPathPrefix(ex.R.URL.RawPath, s.stripPrefix); rawOK { | ||
| ex.R.URL.RawPath = trimmedRaw | ||
| } |
There was a problem hiding this comment.
High Severity: Inconsistent URL state when RawPath trimming fails\n\nIf trimRequestPathPrefix succeeds on Path but fails on RawPath (e.g., due to different escaping or encoding), ex.R.URL.RawPath is left completely untrimmed (retaining the prefix). This creates an inconsistent URL state where Path is stripped of the prefix but RawPath is not.\n\nWhile Go's URL.EscapedPath() may fall back to escaping Path when they are inconsistent, any upstream proxies, routers, or HTTP clients that read URL.RawPath directly will see the untrimmed path (with the prefix still present). This can lead to subtle routing bugs or security bypasses.\n\nTo prevent this, always clear RawPath if trimming fails so that it defaults to a consistent state.
\t\tif trimmedRaw, rawOK := trimRequestPathPrefix(ex.R.URL.RawPath, s.stripPrefix); rawOK {\n\t\t\tex.R.URL.RawPath = trimmedRaw\n\t\t} else {\n\t\t\tex.R.URL.RawPath = ""\n\t\t}| // Filter is a single stage in the gateway request pipeline. | ||
| type Filter interface { | ||
| Handle(context.Context, *Exchange) Result | ||
| } |
There was a problem hiding this comment.
Style Guide Violation: Redundant context.Context parameter in Filter.Handle\n\nAccording to the official Go Code Review Comments style guide:\n> When a function or method takes a Request, it should not also take a Context parameter. Instead, it should use the Request's context.\n\nSince Filter.Handle takes *Exchange which contains ex.R *http.Request, passing a separate context.Context is redundant and can lead to out-of-sync contexts. Furthermore, all 5 filter implementations currently ignore this parameter entirely (_ context.Context).\n\nWe should remove the context.Context parameter from Filter.Handle and have the filters use ex.R.Context() directly.
| // Filter is a single stage in the gateway request pipeline. | |
| type Filter interface { | |
| Handle(context.Context, *Exchange) Result | |
| } | |
| // Filter is a single stage in the gateway request pipeline.\ntype Filter interface {\n\tHandle(*Exchange) Result\n} |
References
- When a function or method takes a Request, it should not also take a Context parameter. Instead, it should use the Request's context. (link)
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 400b981c1b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if ex.Decision.PolicyVersion == "" { | ||
| ex.Decision.PolicyVersion = s.defaultPolicyVersion | ||
| } | ||
| s.emitAuditFromExchange(ex) |
There was a problem hiding this comment.
Skip auditing terminal metadata responses
When policyFilter returns Respond, the unconditional finalization here still audits any request whose body inspection found an RPC method. An unauthenticated client can therefore POST a valid tools/call body to /.well-known/oauth-protected-resource; the metadata handler returns 405 (or 404 in header mode), but the gateway emits an audit event with decision=allow and attacker-controlled RPC/tool fields because authentication and authorization never ran. The previous handler returned before audit emission for this path, so terminal non-upstream responses need to be marked or excluded from emitAuditFromExchange.
Useful? React with 👍 / 👎.
CVE-2026-42504 affects the Go 1.26.3 stdlib; upgrading the multi-stage builder to 1.26.4 (latest stable patch) removes the unfixed CVE so the Trivy operator-image scan no longer fails. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove redundant context.Context from Filter.Handle: all 5 filter implementations already ignored the parameter; filters that need the request context use ex.R.Context() directly (exchange.go, all filter files, proxy.go, pipeline_test.go) - Clear RawPath when path-prefix trimming succeeds on Path but fails on RawPath; previously left an inconsistent URL where the percent-encoded form still carried the prefix, which upstream proxies reading RawPath directly could route incorrectly (filter_upstream.go) - Skip audit for terminal pre-auth responses: policyFilter now sets Exchange.SkipAudit=true before returning Respond for the OAuth metadata early-exit path; emitAuditFromExchange checks the flag so no spurious allow-audit is emitted for requests that bypassed authentication entirely (exchange.go, filter_policy.go, proxy.go) - Add TestPolicyFilterSetsSkipAuditForOAuthMetadataPath test case Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Merge origin/main into mcp-gateway/filter_pipeline, resolving proxy.go conflict by wiring Prometheus request/policy metrics into the pipeline orchestrator while preserving SkipAudit and filter-stage semantics. Co-authored-by: Cursor <cursoragent@cursor.com>
Integrate origin/main policy snapshot behavior; unify errPolicyUnavailable comment for gate filters and validated snapshot readiness. Co-authored-by: Cursor <cursoragent@cursor.com>
Replaces the monolithic handleGateway with a Filter/Exchange/Result pipeline (inspect → policy → auth → authz → upstream). Stage 6 audit runs unconditionally from the orchestrator, centralising all emit paths. errPolicyUnavailable sentinel added so authzFilter fails closed with 503 when no valid policy has been loaded.
Adds per-filter unit tests and ordering proofs in pipeline_test.go: Continue/Reject/Respond per stage, auth-before-authz ordering assertion, authorization-input immutability test, and 503 on policy unavailable.