Stop forwarding inbound client headers to backend MCP servers#62
Stop forwarding inbound client headers to backend MCP servers#62clintonb wants to merge 2 commits into
Conversation
mcp-go stamps every inbound HTTP request's headers onto the parsed request structs (PR #480) and its streamable client sends request.Header verbatim to the backend (PR #546, since v0.43.0). A proxy passing the request struct through therefore forwards the end client's headers — Accept-Encoding, cookies, auth — to backends. An explicit Accept-Encoding disables Go's transparent gzip decompression, which is how still-gzipped gke/datadog responses reached the JSON decoder. Clears request.Header at every backend-forwarding boundary: the aggregate tool handler and the proxy tool, prompt, resource, and resource-template handlers. The gzip guard transport remains as defense-in-depth. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces changes to strip inbound HTTP headers from requests before forwarding them to backend clients, preventing potential header leakage and issues with transparent gzip decompression. This logic is applied across tool, prompt, resource, and resource template handlers, and is accompanied by new unit tests. The review feedback suggests optimizing performance by moving several closure allocations and handler definitions out of loops to avoid redundant allocations.
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.
| "description": tool.Description, | ||
| }) | ||
| // Wrap the tool handler to check for user tokens if required | ||
| callTool := stripInboundHeaders(c.client) |
There was a problem hiding this comment.
Efficiency: Redundant Closure Allocation in Loop
The stripInboundHeaders wrapper is called inside the for _, tool := range tools.Tools loop:
callTool := stripInboundHeaders(c.client)Since c.client is constant across all iterations of the loop, this closure is unnecessarily recreated and allocated for every single tool.
Recommendation:
Move callTool := stripInboundHeaders(c.client) outside of the loop (before the for loop) to avoid redundant allocations.
| for _, prompt := range prompts.Prompts { | ||
| log.Logf("<%s> Adding prompt %s", c.name, prompt.Name) | ||
| mcpServer.AddPrompt(prompt, c.client.GetPrompt) | ||
| mcpServer.AddPrompt(prompt, func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { | ||
| request.Header = nil | ||
| return c.client.GetPrompt(ctx, request) | ||
| }) | ||
| } |
There was a problem hiding this comment.
Efficiency: Redundant Closure Allocation in Loop
Defining the anonymous handler function inside the for loop causes a new closure to be allocated for every single prompt during discovery. Since the handler only interacts with c.client and the incoming request (which is passed as a parameter to the handler), it does not capture any loop-specific variables (like prompt).
We can define the handler once outside the loop and reuse it for all prompts, avoiding unnecessary allocations.
getPrompt := func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
request.Header = nil
return c.client.GetPrompt(ctx, request)
}
for _, prompt := range prompts.Prompts {
log.Logf("<%s> Adding prompt %s", c.name, prompt.Name)
mcpServer.AddPrompt(prompt, getPrompt)
}| mcpServer.AddResource(resource, func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { | ||
| request.Header = nil |
There was a problem hiding this comment.
Efficiency: Redundant Closure Allocation in Loop
Similar to the prompts handler, the anonymous function for reading resources is defined inside the for loop, causing a new closure to be allocated for every resource. Since the handler does not capture any loop-specific variables (like resource), it can be defined once outside the loop to avoid redundant allocations.
For example:
readResource := func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
request.Header = nil
readResource, e := c.client.ReadResource(ctx, request)
if e != nil {
return nil, e
}
return readResource.Contents, nil
}
for _, resource := range resources.Resources {
log.Logf("<%s> Adding resource %s", c.name, resource.Name)
mcpServer.AddResource(resource, readResource)
}| mcpServer.AddResourceTemplate(resourceTemplate, func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { | ||
| request.Header = nil |
There was a problem hiding this comment.
Efficiency: Redundant Closure Allocation in Loop
Similarly, the anonymous function for reading resource templates is defined inside the for loop, causing a new closure to be allocated for every template. Since the handler does not capture any loop-specific variables (like resourceTemplate), it can be defined once outside the loop to avoid redundant allocations.
For example:
readResource := func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
request.Header = nil
readResource, e := c.client.ReadResource(ctx, request)
if e != nil {
return nil, e
}
return readResource.Contents, nil
}
for _, resourceTemplate := range resourceTemplates.ResourceTemplates {
log.Logf("<%s> Adding resource template %s", c.name, resourceTemplate.Name)
mcpServer.AddResourceTemplate(resourceTemplate, readResource)
}The tool, prompt, resource, and resource-template handlers are identical across every discovered item, so hoist them out of the discovery loops to avoid allocating an equivalent closure per item. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Addressed the closure-allocation feedback in 319ca25: the tool, prompt, resource, and resource-template handlers are loop-invariant, so each is now built once before its discovery loop rather than per item. For the tool handler the whole handler (including the |
mcp-go stamps every inbound HTTP request's headers onto the parsed request structs (mark3labs/mcp-go#480, since v0.33), and its streamable client sends a non-nil
request.Headerverbatim as the outgoing request's headers (mark3labs/mcp-go#546, since v0.43.0). A proxy that passes the request struct from its server side to its backend client — which mcp-front's tool, prompt, and resource handlers all do — therefore silently forwards the end client's headers to backends:Accept-Encoding,Cookie, andAuthorization(when the backend config doesn't override it).The visible symptom in production: an explicit forwarded
Accept-Encodingdisables Go's transparent gzip decompression, so backends that honor it (observed with Google's and Datadog's hosted MCP servers) return gzip bodies that reach mcp-go's JSON decoder, failing withinvalid character '\x1f' looking for beginning of value. The failure pattern tracks which end clients send an explicitAccept-Encodingheader, which made it look like random backend flakiness. Beyond the gzip symptom, forwarding inbound credentials-adjacent headers to backends is a header leak in its own right.The fix clears
request.Headerat every backend-forwarding boundary: the aggregate tool handler and the proxy tool, prompt, resource, and resource-template handlers. Unit tests assert the header (includingAccept-EncodingandCookie) is dropped before the backend client sees the request.🤖 Generated with Claude Code