Skip to content

shield: keep the upstream URL's path prefix when forwarding requests - #378

Open
fcy222fcy wants to merge 1 commit into
solongate:mainfrom
fcy222fcy:fix/shield-upstream-path-prefix
Open

fcy222fcy wants to merge 1 commit into
solongate:mainfrom
fcy222fcy:fix/shield-upstream-path-prefix

Conversation

@fcy222fcy

Copy link
Copy Markdown
Contributor

Fixes #331.

The bug

shieldProxyHandler built the upstream target by assigning the incoming request path over the upstream URL's own path:

target := *upstream
target.Path = r.URL.Path
target.RawPath = r.URL.RawPath

So a prefix configured on SOLONGATE_SHIELD_UPSTREAM or ANTHROPIC_BASE_URL — https://gateway.example.com/anthropic, the usual shape for a corporate proxy or a gateway that routes by path — was dropped from every forwarded request. The request went to <host>/v1/messages instead of <host>/anthropic/v1/messages, and the client saw whatever the upstream root path returns. RawPath had the same problem for encoded paths.

shieldPickUpstream parses the URL and this handler is its only consumer of Path, so nothing else in the tree depends on the old behaviour.

The change

The target path is now the request path prefixed with the upstream path (shieldJoinPath), and RawPath follows the same join when the request carried an escaped path.

The request path is prefixed, not cleaned. shield should not rewrite what the client asked for, so dot segments, a trailing slash and percent-encoding all pass through untouched; path.Join would have resolved /v1/../x and dropped a trailing slash instead. The join handles: prefix "" or "/" (old behaviour), prefix with a trailing slash (no doubled slash), a request for the upstream root (/ becomes /llm/), and an empty request path.

Verification

New tests in packages/guard/shield/shield_test.go, all against a live httptest upstream so they assert on the request the upstream really received:

  • TestShieldProxyHandlerKeepsTheUpstreamPathPrefix — upstream URL path /gateway/anthropic, request /v1/messages?foo=bar.
  • TestShieldProxyHandlerKeepsAnEscapedRequestPath — /v1/files/a%2Fb must arrive as /gateway/v1/files/a%2Fb; the %2F has to survive the join.
  • TestShieldProxyHandlerForwardsToTheUpstreamRootWithoutAPrefix — prefixless upstreams ("" and "/") still forward /v1/messages unchanged, so the common case does not regress.
  • TestShieldJoinPath — 10 cases for the join itself, including the no-double-slash case, the upstream root, and dot segments left alone.

Red/green with only the two handler lines reverted (the helper kept, so the package still compiles):

--- FAIL: TestShieldProxyHandlerKeepsTheUpstreamPathPrefix
    shield_test.go:437: upstream saw "/v1/messages?foo=bar", want /gateway/anthropic/v1/messages?foo=bar (the upstream path prefix was dropped)
--- FAIL: TestShieldProxyHandlerKeepsAnEscapedRequestPath
    shield_test.go:455: upstream saw "/v1/files/a%2Fb", want "/gateway/v1/files/a%2Fb" (percent-encoding must survive the join)

With the fix in place those two pass, and so do the existing TestShieldProxyHandlerForwardsAndRedacts and TestShieldProxyHandlerSkipsRedactionForNonJSONBodies. go vet ./... in packages/guard is clean, go vet ./shield is clean for GOOS linux, darwin and windows, and go build ./... is clean.

go test ./shield/ on my Windows machine still reports the same 8 failures before and after the change, so none of them are regressions: seven TestShieldLoadDLPConfig* cases set HOME with t.Setenv, but shared.SGDir() resolves the home directory through os.UserHomeDir(), which on Windows returns USERPROFILE and ignores HOME — the policy file the tests write is never read, so they fall back to the defaults. TestShieldRunChildReportsSignalDeathsAbove128 asserts Unix signal semantics. -race is not available on this machine (no cgo toolchain), so the race build is left to CI.

One behaviour note for anyone whose upstream currently carries a path: this is what makes that prefix work, so a setup that was silently talking to the upstream root will start talking to the prefixed path it configured. I did not touch the case where the prefix and the request path are unrelated to each other, because there is no way to tell a mistake from intent there.

AI disclosure: this fix was located, reproduced and written with the help of an AI coding assistant under my direction.

shieldProxyHandler assigned r.URL.Path over the upstream URL's own Path, so
any prefix configured on SOLONGATE_SHIELD_UPSTREAM or ANTHROPIC_BASE_URL (for
example https://gateway.example.com/anthropic) was dropped and every proxied
request went to the upstream host's root path instead of under that prefix.

Targets now get the request path prefixed with the upstream path. The request
path is only prefixed, never cleaned: dot segments, a trailing slash and
percent-encoding are passed on exactly as the client sent them, so shield still
does not rewrite what was asked for. RawPath follows the same join so escaped
paths survive.

Fixes solongate#331

Signed-off-by: fchengyan <3181088318@qq.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

shield: proxy handler discards the upstream URL's path when forwarding requests

1 participant