Upstream Request Headers
forward_client_headers names the inbound headers a caller sends that AISIX relays to an upstream. It is one field with one meaning on all three faces AISIX proxies through, and it is an array of header-name patterns that is empty by default.
The reason to set it is an upstream that needs something only the caller can supply: a provider beta flag, an application routing hint, a trace correlation header, or — for an internal service that authorizes on the end user rather than on the gateway — the caller's own credential.
Where it lives depends on which face reaches the upstream:
| Field | Applies to |
|---|---|
provider_key.request.forward_client_headers | The standard protocol endpoints (/v1/chat/completions, /v1/completions, /v1/messages, /v1/responses, /v1/realtime, embeddings, rerank, audio, images, videos, and the files, batches, and fine-tuning surfaces) that use this provider key. /v1/realtime carries this field but not default_headers; see below. |
passthrough_route.forward_client_headers | Requests served by that passthrough route. |
mcp_server.forward_client_headers | Tool calls to that MCP server, for both type: mcp and type: openapi. |
Both management paths configure the field on all three resources. A gateway loading a declarative resources file supports everything on this page, and so do the AISIX Cloud Admin API and dashboard — including an entry that names a credential slot exactly, which is what an internal upstream reading the end user's own credential depends on.
The field behaves the same way everywhere — a header a pattern names reaches the upstream — but the default it departs from is not the same on every face, and that changes what setting the field does:
- On the standard endpoints and on MCP, AISIX builds the upstream request from scratch and relays no caller header. The list is an allowlist: it is the only way a caller header reaches the upstream.
- On a passthrough route, AISIX relays the caller's headers by default and strips a small set. The list is an override of that strip set: it only matters for the headers the route would otherwise remove, including the credential slot the gateway just consumed to authenticate the caller.
A provider key has a second, unrelated header setting: request.default_headers adds headers AISIX generates, with values that can reference the current request. Both live on the provider key, so every model that references it inherits them.
Prerequisites
Before starting, prepare the following:
- An upstream provider credential and endpoint. The examples configure these settings on a provider key; see Provider Keys for the rest of that resource's fields.
- For AISIX Cloud, access to an environment, an attached gateway, and a write-scoped admin token. For On-Premises, follow the AISIX Cloud Quickstart. To request Hybrid Cloud access, contact API7.
- For the open-source AISIX gateway, a gateway that loads a declarative resources file.
Configure Upstream Request Headers
Configure the provider key using the management path for your deployment. The settings have the same runtime behavior in both paths.
AISIX Cloud
Export the AISIX Cloud connection details and upstream credential:
# AISIX_CP is the Admin API base URL; include /api and omit a trailing slash.
# The local On-Premises quickstart uses http://localhost:8080/api.
export AISIX_CP="YOUR_AISIX_CLOUD_ADMIN_API_URL"
export AISIX_TOKEN="YOUR_ADMIN_TOKEN"
export ENV_ID="YOUR_ENVIRONMENT_ID"
export UPSTREAM_API_KEY="YOUR_UPSTREAM_API_KEY"
Create a provider key that injects gateway context and relays approved client headers. You can configure either setting independently; this example shows how they can coexist on one key:
curl -sS -X POST "$AISIX_CP/provider_keys" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"display_name": "internal-vllm",
"provider": "byo",
"adapter": "openai",
"api_key": "'"${UPSTREAM_API_KEY}"'",
"api_base": "https://models.internal.example.com/v1",
"allowed_environments": ["'"${ENV_ID}"'"],
"request": {
"default_headers": {
"x-tenant-id": "${request.api_key.team_id}",
"x-audit-context": "key=${request.api_key.id};model=${model.name}",
"x-correlation-id": "${request.id}",
"x-upstream-tier": "premium"
},
"forward_client_headers": [
"anthropic-beta",
"x-routing-hint",
"x-trace-*"
]
}
}'
❶ The team that owns the calling API key. This can identify the tenant for upstream quota or routing decisions.
❷ A value that combines literal text with the calling API key's identifier and the caller-facing model name.
❸ This request's correlation ID, the same value AISIX sends as x-aisix-request-id and reports in its own logs.
❹ A literal value, sent unchanged on every request.
❺ An allowlist of inbound header names and patterns. AISIX still removes the headers it never forwards before sending the request upstream. A credential or trace-context header would have to be named exactly; the x-trace-* pattern here does not match traceparent.
The response includes the provider key ID. Save it when you need to change the header settings later:
export PK_ID="YOUR_PROVIDER_KEY_ID"
Change the Configuration Later
PATCH /provider_keys/{id} replaces the complete request block, and the change reaches every model that references the key. Read the current block first, then send every field you want to retain with the edited values:
curl -sS "$AISIX_CP/provider_keys/$PK_ID" \
-H "Authorization: Bearer $AISIX_TOKEN" \
| jq '.provider_key.request'
curl -sS -X PATCH "$AISIX_CP/provider_keys/$PK_ID" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"request": {
"default_headers": {
"x-tenant-id": "${request.api_key.team_id}"
},
"forward_client_headers": ["anthropic-beta", "x-trace-*"]
}
}'
An empty "request": {} clears the stored override. Catalog provider keys return to the provider's bundled defaults; BYO keys have no request override after the clear. Omitting request leaves the stored block unchanged.
In the dashboard, the same block is under Advanced wire-shape overrides on the provider key's edit form and is pre-filled with the stored values.
Open-Source AISIX Gateway
Add the request block to the provider key entry in resources.yaml. Keep the entry's existing credential fields and preserve unrelated entries and collections. The example uses UPSTREAM_API_KEY, which must be available to the gateway process:
provider_keys:
- display_name: internal-vllm
provider: byo
adapter: openai
api_key: ${UPSTREAM_API_KEY}
api_base: https://models.internal.example.com/v1
request:
default_headers:
x-tenant-id: $${request.api_key.team_id}
x-audit-context: key=$${request.api_key.id};model=$${model.name}
x-correlation-id: $${request.id}
forward_client_headers:
- anthropic-beta
- x-trace-*
In a resources file, every ${NAME} without an escape is substituted from the environment when the file loads. Escape the dollar sign in request-context references as $${...}. The loader converts $$ to a literal $, so $${request.id} reaches the gateway runtime as ${request.id}. An escaped name that is not listed in Available Variables does not resolve at runtime, and AISIX drops that header.
Validate and reload the complete resources file after adding or changing the block. See Reload a Resources File for the complete workflow and the Resources File Reference for the full field catalog.
Request-Context Header Values
Use request.default_headers when the upstream needs to know who is calling. An internal model service can apply per-tenant quotas, attribute cost to a team, or join its access log to the AISIX request ID. These headers avoid requiring a separate upstream credential per tenant.
A value can be a literal string, a ${...} reference, or a combination of literal text and multiple references. AISIX resolves each reference after authenticating the caller and selecting the model.
Available Variables
| Variable | Value |
|---|---|
request.id | Correlation id for this request. |
request.api_key.id | Identifier of the calling API key. |
request.api_key.name | Name of the calling API key. |
request.api_key.team_id | Team that owns the calling API key. |
request.api_key.user_id | Organization member who owns the calling API key. |
model.id | Identifier of the resolved model. |
model.name | Caller-facing name of the resolved model, not the upstream model name. |
provider_key.id | Identifier of this provider key. |
provider_key.name | Name of this provider key. |
Only these variables resolve at request time. The AISIX Cloud Admin API rejects a value that references any other name when you save the provider key. This makes a typo fail immediately instead of becoming a header that never arrives. A resources file has a separate load-time interpolation step, so escape request-context references as described under Open-Source AISIX Gateway.
No variable exposes a secret. Caller API keys, upstream credentials, and signing material are not part of the request context a header value can read.
A header whose variables do not all have a value for a request is dropped from that request rather than sent empty. If the calling API key belongs to no team, a request through the example above carries x-audit-context and x-correlation-id, but no x-tenant-id. An empty x-tenant-id would tell the upstream that the tenant is the empty string.
Protected Header Names
request.default_headers cannot set the names no configuration puts on an upstream request: host, the hop-by-hop set, and the gateway's own x-aisix-* namespace. Both management paths agree on that boundary — AISIX Cloud rejects these names when you save the provider key, and a gateway reading a resources file drops them at dispatch time. They are the first group under Caller Headers AISIX Never Forwards, which binds default_headers whatever the header's source. The second group there applies only to headers received from the caller, so it places no restriction on default_headers.
A credential name is not among them. Naming authorization, x-api-key, or another credential slot in default_headers is how an upstream that reads a second, static credential gets one — on a provider whose own credential goes elsewhere.
What such an entry cannot do is displace a header AISIX already set. A default header fills a slot the selected provider bridge left empty; it never replaces the upstream credential or content-type.
Forward Client Headers
List each header as an exact name or a name containing a single * wildcard. Matching is case-insensitive, so X-Trace-* and x-trace-* are the same pattern and both match x-trace-id. An empty or absent list — the default — forwards nothing and overrides no stripping.
Callers cannot opt themselves in. The list is operator configuration on the upstream-facing resource, and a header a caller sends that no entry names is handled exactly as it would be without the field.
On the standard endpoints and MCP, a caller that sends the same header more than once has its first value forwarded and the rest dropped, so the upstream receives one well-formed header rather than a list the gateway never interpreted. A passthrough route relays the caller's headers as they arrived, repeats included.
Some requests have no caller behind them at all — a background poll of an asynchronous job, or the embedding lookup a semantic router makes. Those forward nothing regardless of configuration, because there is no inbound request to take a header from.
A /v1/realtime WebSocket forwards the headers its provider key's list names, like the other standard endpoints. Three things about that face are its own:
- It refuses the five handshake slots it owns —
sec-websocket-accept,sec-websocket-extensions,sec-websocket-key,sec-websocket-protocol, andsec-websocket-version— on top of the names no pattern reaches anywhere. They describe the handshake the caller opened to AISIX, not the one AISIX opens upstream. - No pattern reaches those five, including one that names a header in full: this face checks its own refusals before it consults the list.
sec-websocket-protocolis the one that matters most, because the browser flow carries the caller's own AISIX key as an item in that list. request.default_headersdoes not apply here. One half of the provider key'srequestblock takes effect on this face and the other half does not.
A forwarded value that is not ASCII is dropped from the handshake rather than failing the session, because this face writes its upstream handshake as text. Every other face forwards the same value byte for byte.
Configure It on a Route or an MCP Server
On a passthrough route and an MCP server the field is top-level rather than inside a request block, and it takes the same patterns.
Through the AISIX Cloud Admin API, patch the resource with the complete list you want it to have. ROUTE_ID and SERVER_ID are the IDs returned when the route and the server were created:
curl -sS -X PATCH "$AISIX_CP/environments/$ENV_ID/passthrough_routes/$ROUTE_ID" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{"forward_client_headers": ["authorization", "x-trace-*"]}'
curl -sS -X PATCH "$AISIX_CP/mcp_servers/$SERVER_ID" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{"forward_client_headers": ["authorization"]}'
A patch replaces the stored list rather than adding to it. An empty array clears it on either resource, and a passthrough route additionally accepts null, which an MCP server does not. Omitting the field leaves the stored list unchanged on both.
In the dashboard, the same setting is a Forward client headers box under Advanced on the passthrough route and MCP server forms, one header name or glob per line.
Configure it in the resources file an open-source AISIX gateway loads:
passthrough_routes:
- name: internal-copilot
path_prefix: /internal
target_url: https://models.internal.example.com
auth_mode: gateway_key
# `inject` is the default and would require a provider key to inject.
credential_mode: forward_client
forward_client_headers:
- authorization
- x-trace-*
mcp_servers:
- name: runbooks
type: mcp
url: https://runbooks.internal/mcp
# No gateway credential: the caller's own is what this server reads.
auth_type: none
forward_client_headers:
- authorization
Headers That Must Be Named Exactly
A wildcard does not sweep up a header AISIX itself consumes as a credential, nor a W3C trace-context header. Forwarding one of those is an explicit act, and naming it in full is what forwards it.
The headers that need their own entry on every face are:
| Headers | Kind |
|---|---|
authorization, proxy-authorization, x-api-key, api-key, x-goog-api-key, cookie | Credential slots |
traceparent, tracestate | W3C trace context |
A passthrough route adds the two slots it names for itself. AISIX consumes both, and the shared list cannot know the name a given route chose for either:
| Headers | Kind |
|---|---|
The route's auth_header_name | The gateway credential, under auth_mode: header_key |
The route's identity_header | The end-user identity the route records and strips |
Naming either in full forwards it, and a glob does not reach it. Without that, a route configured with ["x-*"] would relay the very header AISIX had just consumed to authenticate the caller, or the identity value the route promises to strip.
Writing * or x-* is a statement about your own headers. It is not consent to hand a third-party provider the caller's credential, nor to graft the caller's trace onto that provider's telemetry — both of which a broad glob would otherwise do the moment a caller happened to send the header. Naming the header in full is that consent, and is all that is required: "forward_client_headers": ["authorization"] forwards the caller's Authorization on every face.
This also keeps an existing broad pattern meaning what it meant when it was written. A provider key configured with ["x-*"] does not begin relaying the caller's x-api-key — which on /v1/* is the caller's own AISIX gateway key — because the gateway was upgraded.
Forwarding the Caller's Credential
Naming a credential slot is how an internal upstream that already authorizes on the end user keeps doing so with AISIX in front of it. The caller's value takes that slot from the credential AISIX would otherwise have injected there:
- On a provider key, in place of the key's own credential.
- On an MCP server, in place of the credential
auth_typewould fill —authorizationforbearerandoauth2, and forapi_keythe headerapi_key_headernames (x-api-keyunless atype: openapiserver overrides it). - On a passthrough route, in place of the injected provider credential, and instead of the strip that
gateway_keyauthentication would otherwise apply toauthorizationandx-api-key.
An MCP server's api_key_header is reachable by a glob unless it happens to be one of the exact-name headers. Its default, x-api-key, is a credential slot and so needs its own entry; a type: openapi server that renames the slot — to x-mcp-token, say — gives it a name that ["x-*"] matches, and a caller sending that header then supplies its own upstream credential.
The slot is single-valued either way: AISIX replaces rather than appends, so the upstream receives one credential and never chooses between two. AISIX still authenticates the caller as usual first — this setting changes only what the upstream sees, never who AISIX believes is calling.
The forwarded credential is whatever the caller put in that slot — not necessarily an end-user identity token. On /v1/*, on a gateway_key passthrough route, and for every MCP client, the caller's Authorization is the AISIX caller API key itself, so naming authorization sends a live gateway credential upstream. Name the slot only for an upstream you would trust with that value.
The upstream must also be one that accepts it: an upstream that validates an audience claim rejects a token minted for the gateway. Do not name a credential slot on a public model provider.
Only a credential slot displaces something AISIX already set. Any other header AISIX put on the request, it put there to make the exchange work — a provider's asynchronous-mode flag or API-version selector — so a forwarded header of that name is dropped rather than allowed to break the call.
W3C Trace Context
AISIX reads the caller's traceparent and tracestate as telemetry input. One valid inbound traceparent makes the gateway's HTTP SERVER span a child of the caller's span. A malformed value, or more than one traceparent, starts a new local trace instead of failing the request. AISIX keeps tracestate only when traceparent is valid. See OTLP Trace Structure for the spans AISIX exports.
Reading the context is separate from relaying it. By default AISIX does not send the caller's trace headers upstream, on any face. Name traceparent or tracestate exactly in forward_client_headers to relay them as well — appropriate for an internal upstream that reports into the same tracing backend, and not for a third-party provider.
Caller Headers AISIX Never Forwards
Some headers no pattern can reach. These restrictions exist because forwarding the header would break the exchange rather than change who it comes from, so they apply regardless of configuration.
They also apply only to caller-supplied values. AISIX still sends its own value for several of these names: it sets content-type for the body it builds and stamps x-aisix-request-id.
The first group applies on every face:
| Header group | Headers | Why |
|---|---|---|
| Host | host | Selects which server the request reaches at all. |
| Hop-by-hop | connection, keep-alive, te, trailer, transfer-encoding, upgrade, proxy-authenticate | These describe the connection the caller opened to AISIX, not the one AISIX opens to the upstream. |
| Gateway-owned | x-aisix-* | Assertions AISIX makes about a request it handled. A caller's copy would forge them upstream and lose the assertion itself. A header named in proxy.request_id.accept_headers — x-aisix-request-id by default — is an exception only in that AISIX reads the request ID from it; it then emits that value as its own header, so the upstream receives a single value. |
The standard endpoints and MCP rebuild the outbound message, so they exclude a second group as well:
| Header group | Headers | Why |
|---|---|---|
| Body and negotiation | content-type, content-length, content-encoding, accept, accept-encoding, expect | They describe a body AISIX re-serializes and a response shape it parses, so the caller's copies would describe the wrong message. |
| Response-only | set-cookie | A response header with no meaning on a request. |
| Provider wire format | anthropic-version | Selects the wire format an Anthropic-shaped upstream answers in, which AISIX then decodes. A caller's value breaks the decode. |
| Client SDK | x-stainless-* | Version headers the caller's SDK sends about itself. Relaying them to a provider that reads the same headers for its own SDK breaks the call. |
Three faces exclude a little more of their own:
- MCP also never forwards
mcp-session-id,mcp-protocol-version, andlast-event-id. They name the session the caller holds with AISIX, not the one AISIX opens upstream, and an upstream MCP server refuses a session id it never issued. - Passthrough routes relay the body verbatim, so the second group above does not apply to them —
content-typesurvives. They excludecontent-lengthon top of the first group, because the outbound client derives the length from the body it is handed and a relayed value is a request-framing bug. /v1/realtimealso never forwardssec-websocket-accept,sec-websocket-extensions,sec-websocket-key,sec-websocket-protocol, andsec-websocket-version. They describe the handshake the caller opened to AISIX, not the one AISIX opens upstream, and no pattern reaches them even when it names one in full.
One provider-specific exception: on an AWS Bedrock provider key, the headers AWS SigV4 derives from the request it signs — authorization, x-amz-date, x-amz-content-sha256, x-amz-security-token, x-amz-target, and x-amzn-bedrock-accept — are dropped from both forward_client_headers and default_headers. A supplied value there would break the signature rather than authenticate anyone.
Precedence
When the same header name comes from more than one place, AISIX resolves it as follows:
request.default_headersbeats a header relayed byrequest.forward_client_headers— both are operator configuration and the static one is the more specific statement of intent — for every name except a credential slot. A forwarded value takes a credential slot from adefault_headersentry as readily as from the gateway's own.- A
default_headersentry never replaces a header AISIX set itself, including the upstream credential,content-type, andx-aisix-request-id. - A forwarded caller header replaces a header AISIX set itself only when the name is a credential slot. For every other name, the value AISIX set stands.
- On a passthrough route,
forward_client_headersbeats the provider key'sstrip_headers. A stripped name is not permanently barred: undercredential_mode: inject, a route whose list is["x-*"]puts a strippedx-header back on the upstream request, because a glob is all an ordinary header name needs. The exact-name rule still holds for a credential or trace-context header and for the route's own two slots, and the headers AISIX never forwards stay out whatever the patterns say.
A header is single-valued on the wire throughout: AISIX replaces rather than appends, so an upstream never receives both an operator value and a caller value for the same name.
Verify
Export a caller API key and model alias that can use a model referencing the provider key. Then send a request with a header the allowlist names:
# AISIX_PROXY is the gateway origin; omit a trailing slash and endpoint path.
# The local quickstarts use http://127.0.0.1:3000.
export AISIX_PROXY="YOUR_AISIX_GATEWAY_ORIGIN"
export AISIX_API_KEY="YOUR_CALLER_API_KEY"
export MODEL_ALIAS="YOUR_MODEL_ALIAS"
curl -sS -X POST "$AISIX_PROXY/v1/chat/completions" \
-H "Authorization: Bearer $AISIX_API_KEY" \
-H "Content-Type: application/json" \
-H "x-trace-id: trace-0001" \
-d '{
"model": "'"${MODEL_ALIAS}"'",
"messages": [{"role": "user", "content": "ping"}]
}'
Confirm the expected headers in the upstream service's access log or equivalent request telemetry.
If you point api_base at a request-inspection service instead, remember that AISIX sends the provider key's credential and the request body to whatever that api_base names. Use an endpoint you control, a throwaway credential on the test provider key, and a prompt that carries no real data.
If an expected header is missing, check the following:
- The header name is listed in
forward_client_headers, or matches one of its wildcard entries. - The header is not in Caller Headers AISIX Never Forwards.
- A credential or trace-context header is named exactly, not matched by a wildcard entry. On a passthrough route, the same holds for the route's own
auth_header_nameandidentity_header. - On
/v1/realtime, the header is not one of the handshake slots that face refuses, and it is not adefault_headersentry — that half of therequestblock does not apply there. - For a
default_headersvalue with a variable, the calling API key actually has that attribute. A key with no team drops a header that referencesrequest.api_key.team_id. - In a resources file, each request-context reference escapes the load-time environment interpolation as
$${...}.
Next Steps
- Provider Keys for the rest of the provider-key configuration, including compatibility overrides.
- Resources File Reference for the declarative field catalog.
- Passthrough Routes and MCP Upstream Authentication for the other two faces this field appears on.