Skip to content

feat: forward client request headers to upstream providers in bridge routes#214

Open
ssncferreira wants to merge 1 commit intomainfrom
ssncf/feat-forward-client-headers-mdlw
Open

feat: forward client request headers to upstream providers in bridge routes#214
ssncferreira wants to merge 1 commit intomainfrom
ssncf/feat-forward-client-headers-mdlw

Conversation

@ssncferreira
Copy link
Contributor

@ssncferreira ssncferreira commented Mar 12, 2026

Description

AI Bridge acts as a reverse proxy between clients and upstream AI providers. However, bridge routes were dropping client request headers as the interceptors construct new HTTP requests via provider SDKs, which set their own headers and discard the originals.

This PR changes bridge routes to forward client request headers to upstream providers, so the upstream sees the same headers the client sent, except headers related to auth, transport, or managed by aibridge.

Changes

  • Add intercept/client_headers.go with SanitizeClientHeaders and BuildUpstreamHeaders. Client headers are sanitized by stripping hop-by-hop (RFC 2616), transport (Host, Accept-Encoding, Content-Length), and auth (Authorization, X-Api-Key) headers.
  • Add SDK middleware in all three interceptor base types that replaces the SDK-built request headers with the sanitized client headers. Two categories of headers are preserved from the SDK-built request:
    • Auth headers: the SDK sets the correct provider credentials (API key or per-user token for Copilot), and these must not be overwritten by the client's auth header.
    • Actor headers (X-AI-Bridge-Actor-*): injected by aibridge as per-request SDK options to identify the requesting user to the upstream.
  • The client-header middleware is appended before the API dump middleware, so that API dump captures the final outgoing headers as they are sent to the upstream.

This is an intermediate step toward removing the SDK from the HTTP transport path. By forwarding client headers directly, aibridge is no longer dependent on SDK-managed headers, making future SDK removal simpler.

Follow-up

  • Proxy headers: Passthrough routes set standard proxy headers (X-Forwarded-For, X-Forwarded-Host, X-Forwarded-Proto), while bridge routes do not. Since AI Bridge behaves as a reverse proxy, this handling should be consistent across all requests.
  • Inject actor headers directly in the client-header middleware instead of using per-request SDK options.
  • Remove ExtraHeaders mechanism for Anthropic and Copilot: now redundant since all client headers are forwarded.

Tests

Tested manually with:

  • ✅ Claude Code 2.1.74
  • ✅ codex-cli 0.114.0
  • ✅ Copilot VS Code 0.38.2 (via AI Bridge Proxy)

Closes: #192

Copy link
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@ssncferreira ssncferreira force-pushed the ssncf/feat-forward-client-headers-mdlw branch 3 times, most recently from a2da2f0 to 252f03a Compare March 12, 2026 19:57
@ssncferreira ssncferreira force-pushed the ssncf/feat-forward-client-headers-mdlw branch from 252f03a to 2d43f46 Compare March 12, 2026 20:16
Comment on lines +91 to +92
// TODO(ssncferreira): inject actor headers directly in the client-header
// middleware instead of using SDK options.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will handle this in a follow-up in order to not increase the scope of this PR.


// nonForwardedHeaders are transport-level headers managed by aibridge or
// Go's HTTP transport that must not be forwarded to the upstream provider.
var nonForwardedHeaders = []string{
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I noticed is that Copilot sends a Cookie header (e.g. from API dump: Cookie: csrf...N0E=). I assume this is used for session purposes between Copilot and Github, but do you see any issue in forwarding as is upstream?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should make many judgment calls on these sorts of things. Whatever the client sends is what we should deliver.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not an expert on Cookies but from quick research I don't see why we shouldn't forward them as received.


// nonForwardedHeaders are transport-level headers managed by aibridge or
// Go's HTTP transport that must not be forwarded to the upstream provider.
var nonForwardedHeaders = []string{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should make many judgment calls on these sorts of things. Whatever the client sends is what we should deliver.

}

// authHeaders are headers that carry authentication credentials from the
// client. These are stripped because the SDK re-injects the correct
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: "the SDK re-injects" can be reworded to be more explicit.
Which SDK? Where does it do that?

(i know the answers but future readers may not)


// SanitizeClientHeaders returns a copy of the client headers with hop-by-hop,
// transport, and auth headers removed.
func SanitizeClientHeaders(clientHeaders http.Header) http.Header {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Sanitize is perhaps the wrong term here. Maybe Prepare?

Since we're acting (almost) like a reverse-proxy, we should be adding in the X-Forwarded-For, X-Forwarded-Host, X-Forwarded-Proto headers here, no?

mockUpstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedHeaders = r.Header.Clone()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: can we use a fixture instead? It'll make the tests a bit more concise.

Copy link
Contributor

@pawbana pawbana left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall LGTM although it is a bit hard to review new and changes to the tests 😅


// nonForwardedHeaders are transport-level headers managed by aibridge or
// Go's HTTP transport that must not be forwarded to the upstream provider.
var nonForwardedHeaders = []string{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not an expert on Cookies but from quick research I don't see why we shouldn't forward them as received.


var receivedHeaders http.Header

mockUpstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would internal/integrationtest/mockupstream.go be useful here. Probably new method that creates upstreamResponse from string/bytes would be need.

I see now it may not have been a good idea to make it private 😞

func TestOpenAI_CreateInterceptor(t *testing.T) {
t.Parallel()

t.Run("ChatCompletions_ClientHeaders", func(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for begin pedantic about this but would it be possible to reformat this into table tests? 😅
IIUC both tests are doing basically the same but with different request path?

Maybe I'm just used to it but I feel table test format is much easier to understand and keep track of test cases. It usually also shortens the code.

From my experience AIs for some reason don't do write tests in table format first but when you ask for it directly in follow up prompt AI realizes it is possible or sometimes provides a reason why it is not worth it.

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.

Forward client request headers to upstream providers

3 participants