Repository navigation
fix(agent-gateway): stop duplicating HTTP tool query arguments in the upstream URI - #7926
Merged
Aias00 merged 1 commit intoOct 22, 2026
Conversation
… upstream URI AgentHttpToolExecutor stored the route together with its encoded query in ShenyuContext.realUrl while the child request URI already carried the same query. RequestUrlUtils.buildRequestUri appends the request query to realUrl, so every configured GET tool reached its upstream as /orders/status?id=A?id=A. Keep only the route path in realUrl (raw path, matching the plugin contract) and let the child request URI carry the encoded arguments. Build the child URI once and reuse it for the context fields. Verified: the new regression test fails on the previous code with 'Invalid character = for QUERY_PARAM in "A%26B?id=A%26B"' and passes with the fix; POST body arguments and a queryless route stay unchanged.
Aias00
approved these changes
Oct 21, 2026
Aias00
left a comment
Contributor
There was a problem hiding this comment.
LGTM — the diagnosis is right and the fix is at the right place.
What I verified
- The duplication was real and had exactly the shape described.
RequestUrlUtils.buildRequestUricomposesdomain + shenyuContext.getRealUrl()and then appends the query ofexchange.getRequest().getURI(). Since the child request is built withuri(target)wheretargetalready carries the encoded arguments, arealUrlthat also carried them produced...?id=A%26B?id=A%26B. Dropping the query fromrealUrlis the correct side to fix: the query is owned by the child request URI, so nothing is lost even when the tool template has its own literal query (e.g./orders/status?fixed=1) — that fragment lives in the child's raw query and is re-appended bybuildRequestUri. getPath()→getRawPath()is the right call, not just a cosmetic change.DefaultShenyuContextBuilder.buildDefaultContextdoesshenyuContext.setPath(requestURI.getRawPath()), so the previousURI.create(path).getPath()made an agent tool call match selectors against a decoded path while ordinary traffic matches against the raw one. Using the sametargetURI for bothsetPathandsetRealUrlalso removes the secondURI.create(path).- The tests assert against the real
RequestUrlUtilsrather than a stub, soforwardsEncodedQueryArgumentsExactlyOnceThroughTheOrdinaryHttpRouteproves the byte-level upstream URI (?id=A%26Bonce, withA&Bcorrectly escaped on the way in) instead of asserting on an intermediate string.keepsTheContextPathRewriteWithoutDuplicatingTheQueryadditionally runs the realContextPathPlugin, which is the plugin that rewritesrealUrland could have re-introduced a query — that is the test that would catch a future regression here. shenyu-plugin-context-pathis only needed at test scope for theContextPathPluginfixture, and it does not depend back onshenyu-plugin-agent-gateway, so the new edge introduces no reactor cycle.
Non-blocking
setPathmoving from decoded to raw is matching-visible for a tool URL whose path contains a percent escape (e.g./my%20route/x): it now matches the same selectors the ordinary chain would, which is the intent, but none of the new tests pincontext.getPath()— they only assertrealUrland the built URI. One assertion on((ShenyuContext) child.getAttribute(Constants.CONTEXT)).getPath()in the encoded-argument test would lock the behaviour in, and a line in the PR description would flag it as an intentional side effect of the fix.@BeforeEachinstalls a mockedConfigurableApplicationContextviaSpringBeanUtils.getInstance().setApplicationContext(context)— a JVM-wide static — and@AfterEachcleans the plugin caches but leaves that context installed. Every test that runs later in this module now sees the mock. You clearly care about isolation here (the cache cleanup is thorough), so capturing the previous context and restoring it would close the last gap.forwardsABodyArgumentWithoutInventingAQueryandforwardsAQuerylessRouteWithoutAddingAQuestionMarkpass on the pre-fix code as well — in neither shape was a query ever produced. They are useful guards, but only the two query-carrying tests actually fail before the fix, which is worth stating in the PR body so a future reader knows which assertions are the regression proof.realUrlbeing path-only now depends on the child request URI always carrying the arguments. That holds today because the arguments are encoded intotarget, and the added comment explains it — good. If the argument placement ever moves out of the URI (headers, body), the coupling breaks silently, so the comment is load-bearing; consider also assertinggetRealUrl()has no?in the encoded-argument test to make that contract explicit.
Note on CI: build (shenyu-integrated-test-http) is currently red on master because of the CryptorResponsePluginTest.testWhenDoesntExistFiled assertion left behind by #7903 — unrelated to this PR.
1 task done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #7909.
Problem
A configured HTTP tool with a query argument reaches the upstream with the query twice:
AgentHttpToolExecutorput the whole route, query included, intoShenyuContext.realUrl(context.setRealUrl(path)), while the child request URI already carried the same encoded query.RequestUrlUtils.buildRequestUriappends the request query torealUrl, so the ordinary HTTP route concatenated both copies.Changes
AgentHttpToolExecutornow builds the child URI once and stores onlytarget.getRawPath()inrealUrl(andpath), matching what the other context builders put there (DefaultShenyuContextBuilder,DivideShenyuContextDecorator). The encoded arguments stay on the child request URI, which is the single source of the query.shenyu-plugin-context-pathas a test-scope dependency so the context-path case can be exercised with the real plugin.Validation
Regression tests in
AgentHttpToolExecutorTestbuild the final upstream URI with the productionRequestUrlUtils.buildRequestUri, the same callURIPluginmakes, instead of only asserting the child request URI:forwardsEncodedQueryArgumentsExactlyOnceThroughTheOrdinaryHttpRoute— GET,id=A&B→http://127.0.0.1:18080/orders/status?id=A%26B.forwardsABodyArgumentWithoutInventingAQuery— POST withargsPosition=bodykeeps the URI queryless.forwardsAQuerylessRouteWithoutAddingAQuestionMark— no arguments, no trailing?.keepsTheContextPathRewriteWithoutDuplicatingTheQuery— route/api/orders/statuswith a context-path rule/apistill forwards/orders/status?id=A%26Bafter the realContextPathPluginrewritesrealUrl.Failure before the fix (main code reverted, tests kept):
Passes with the fix. Results:
./mvnw test -pl shenyu-plugin/shenyu-plugin-agent-gateway: 550 tests, 0 failures; 27 errors are alljava.io.IOException: Unable to establish loopback connection/SocketException: Invalid argument: connectfrom tests that open a real local socket (RequestScopedMcpClientTest,ManagedRemoteMcpCatalogTest, oneAgentMcpOutputSchemaValidatorTestcase). The same failures reproduce on this machine with an unrelated 3-lineSelector.open()program, so they are environmental and are left to CI.AgentHttpToolExecutorTest27/27,AgentHttpToolRoutingTest13/13,AgentGatewayPluginTest17/17.checkstyle:check: 0 violations.apache-rat:check: 0 unapproved licenses.Blast radius
realUrlis read byRequestUrlUtils.buildRequestUriandgetRewrittenRawPath,RewritePlugin, andAiRequestTransformerPlugin. Tool calls setpathandrealUrlto the same value, so the "already rewritten" guard in the AI transformer and the selector/rule matching ingetRewrittenRawPathkeep seeing the route path, which is what they saw before the query was added torealUrl— the only observable difference is that the duplicated query is gone.Out of scope
Remote MCP tools and the MCP Server plugin's own
ShenyuToolCallbackpath already strip the query withextractRawPath; they are not touched here.