Docs

AISIX 1.1.0

This release hardens the path between the control plane and the gateway.

Release date: September 7, 2026

This release hardens the path between the control plane and the gateway. A gateway now opens its proxy port only once it has a configuration to serve, recovers on its own from an etcd outage, an expired etcd token, or a watch that is never confirmed, and loads configuration sets that previously exceeded an internal message-size ceiling. A2A agents complete the client-header forwarding contract, and the On-Premises control-plane chart gains fixed NodePorts and stops waiting on a bundled database it was told not to use.

Behavior Changes

  • The gateway binds its proxy listener only after it has applied a configuration. In etcd mode a cold-booting instance keeps the proxy port closed until its first configuration read succeeds, instead of accepting connections immediately and answering every request 401 invalid_api_key because it knew no API keys yet. An orchestrator that treats "the port accepts" as "this instance is ready" no longer routes traffic to an instance that has nothing to serve. There is no configuration knob and no opt-out; this is the startup order now. The wait only ever applies to an instance that has never applied a configuration. File mode passes the gate immediately, and so does a restart that finds an on-disk snapshot cache: such an instance restores the snapshot and binds in well under a second even while its configuration source is unreachable, so a pod that has run before comes straight back up during a control-plane outage. A first boot with no cached snapshot is the only case that waits, and against a reachable control plane that wait is tens of milliseconds at realistic configuration sizes. /livez and /readyz are served on the proxy listener, so a probe aimed at that port gets a refused connection rather than a response during the window; the same two paths on the admin listener, and /status/ready and /status/config on the metrics listener, are bound before the gate and answer throughout. Helm users should take the 1.1.0 chart alongside the 1.1.0 image: the chart's startup budget was widened to match this order.

  • The etcd.dial_timeout_ms and etcd.request_timeout_ms settings now take effect. Both were parsed and then read by nothing in earlier releases, so every deployment ran with no etcd timeouts whatever the file said. Both keys are optional and unset means unbounded; there is no implicit 5000. request_timeout_ms bounds each single request and response etcd call — the configuration range read at boot and on every watch reconnect, creating the watch, and the admin API's reads — and never the established watch stream. dial_timeout_ms bounds the whole dial, including the TLS handshake and the authentication exchange. A configuration file copied from the previous release's shipped examples already carries both keys at 5000, so review it before upgrading; see Upgrade Notes.

  • A gateway with etcd credentials configured no longer exits when etcd is unreachable at startup. Previously, setting etcd.user and etcd.password_env made an unreachable etcd fatal: the process worked through a fixed ladder of five connection attempts five seconds apart and then exited, roughly 20 to 25 seconds in, without ever binding a listener. Such an instance now behaves like an unauthenticated one — it starts, holds the proxy listener closed, retries on its existing backoff, and binds as soon as a configuration arrives. Credentials that etcd actively refuses — a wrong user or password, a user without the required permission, or credentials sent to a cluster with authentication disabled — remain fatal, and now stop the boot on etcd's first answer rather than after a fixed retry ladder. Anything that relied on the process exiting to signal "etcd is down" should read /status/ready instead.

New Features

  • A2A agents can forward inbound client headers upstream. A new forward_client_headers list on an A2A agent names the client headers the gateway relays to that agent, as single-* glob patterns matched case-insensitively. It applies to every JSON-RPC method served at /a2a/<name> — message/send, message/stream, and every task operation — and to the agent-card fetch at /a2a/<name>/.well-known/agent-card.json. This completes the forwarding contract across all four proxy faces, alongside Provider Keys, passthrough routes, and MCP servers. Empty is the default and forwards nothing, so no existing agent changes behavior.

    Naming the credential slot the agent's auth_type fills — authorization for bearer, x-api-key for api_key — hands the agent the caller's own credential in place of the gateway's, never both, which lets an internal agent that already authorizes on the end user's identity keep doing so. A credential slot and the traceparent and tracestate headers are forwarded only when a pattern names them exactly; a broad glob such as * or x-* never sweeps them in. Headers whose relay would break the exchange are refused however broad the pattern: host, hop-by-hop headers, the x-aisix-* namespace, the headers describing a body the gateway re-serializes, and a2a-version, which is the gateway's own announcement of the version pinned in protocol_version.

    One limitation is worth knowing before you forward cookie. The gateway terminates inbound HTTP/2 and performs no cookie reassembly, so an HTTP/2 caller that splits cookie across several header fields — an HPACK compression option that not every client exercises — has only the first of them forwarded. This is true of A2A agents, MCP servers, and Provider Keys alike; a passthrough route relays every value.

    The setting is available in the Dashboard on the A2A agent create and edit forms under Advanced, and through the Admin API. Saving a non-empty list while some gateways in scope still run a version that predates it returns a compatibility warning; those gateways relay nothing until they are upgraded, which is the fail-closed direction.

Improvements

  • The gateway loads configuration sets larger than 4 MiB. The gateway reads its whole prefix in a single etcd range response, and a 4 MiB gRPC decode ceiling meant an environment that grew past that size never loaded — a limit crossed by how many resources an environment holds, not by any one of them being large. It did not self-heal: a running instance silently stopped seeing configuration changes, and a restarted or newly scheduled one never applied a configuration at all. The ceiling is lifted on both the configuration read path and the admin read surface. Deployments below the old limit are unaffected.

  • The control plane no longer caps the size of a resource it writes to the gateways. Two message-size ceilings on the control plane's own write path — a 2 MiB send limit on the outbox client and a 4 MiB receive limit on the embedded store — could reject a single oversized projected resource on every poll, forever. Per-key isolation meant the rest of the queue kept draining, so the visible result was one resource whose configuration never reached the gateways, with nothing surfaced to whoever saved it. Neither ceiling was chosen for this path and both are removed. Reaching either required an unusually large projected value, such as an API key granting many thousands of models.

  • A gateway waiting on its configuration source now says so. While the first configuration read is outstanding, the gateway logs when the wait starts and repeats a warning every ten seconds. An endpoint that accepts TCP and then answers nothing previously produced a process with no port and no log line at all.

  • The On-Premises control-plane chart accepts fixed NodePorts for the API and Dashboard services. api.service.nodePort and ui.service.nodePort join the existing dpm.service.nodePort, each rendered only when its Service type is NodePort and a value is set; both default to empty, so an installation that does not set them keeps dynamic allocation. This lets an operator set api.publicBaseURL to a known endpoint before installation. NodePort access to the API and Dashboard is plain HTTP: the Dashboard NodePort is not a standalone entry point but the upstream of a same-origin proxy that routes /api/* to the API, and that proxy should terminate TLS unless it is on a trusted private network.

Fixes

  • An expired or invalidated etcd authentication token no longer requires a gateway restart. The etcd client authenticated once at connect and never again, so a token whose lifetime elapsed while the connection was idle — the normal state of a gateway with stable configuration — or one invalidated by a change to etcd's authentication store left every later call refused until the process was restarted. The gateway now discards the connection, re-authenticates, and retries the call once, on both the configuration read path and the admin read surface. Credentials etcd genuinely refuses are still reported as such and are not retried in a loop.

  • The gateway no longer loses its last configuration snapshot when stopped shortly after an update. Snapshot-cache writes were spawned detached and nothing waited for them, so an instance stopped soon after applying a change could exit with the write unfinished and restart without its last-known-good snapshot. In-flight writes are now drained at shutdown, bounded at five seconds. Snapshot writes also commit in apply order: a write for an older revision no longer overwrites a newer one, so a gateway restarted after a burst of updates cannot come back onto a snapshot it had already moved past. The graceful-drain sequence, the drain window, and preStop behavior are unchanged.

  • A configuration watch that is never confirmed is now detected. An etcd that answered range reads but never confirmed the watch left the gateway serving its first snapshot forever, blind to every later change, while /status/config still reported it connected. When request_timeout_ms is set, an unconfirmed watch creation is aborted and retried on the existing backoff.

  • MCP access-control reprojection now covers every affected row. The reprojection that refreshes MCP access control on upgrade paginated in a way that could skip rows, leaving those rows projected in the retired shape the flattening was meant to replace. It now walks every API key and MCP policy explicitly. Upgrading the control plane performs one additional projection write per current API key and MCP policy. No API, configuration, or resource shape changes, and no operator action is required.

  • The On-Premises control-plane chart no longer waits for the bundled PostgreSQL when an external database is configured. Setting postgresql.builtin=false disabled the bundled server, but the API and Data Plane Manager still ran a PostgreSQL readiness init container built from postgresql.image.*, so in a private network an unavailable PostgreSQL image could block startup even with a healthy external database. The readiness init is now rendered only when postgresql.builtin=true. Bundled deployments are unchanged and remain the default. Operators using an external database must ensure it is ready before the control plane starts.

API Changes

The OpenAPI structural comparison reports no breaking changes. All additions are optional and confined to the A2A agent surface.

EndpointChange
  • GET /a2a_agents
  • GET /a2a_agents/{a2a_agent_id}
A2A agent representations add optional forward_client_headers.
  • POST /a2a_agents
  • PATCH /a2a_agents/{a2a_agent_id}
Accept optional forward_client_headers, an array of header-name glob patterns; [] clears an existing list. Entries are validated against the same rules as the other forwarding surfaces — RFC 7230 token grammar, at most one * per entry, and a 64-entry cap — and a pattern that could only ever name a header this surface never relays is rejected with 400. A PATCH carrying only forward_client_headers is a valid update.
  • POST /a2a_agents
  • PATCH /a2a_agents/{a2a_agent_id}
Successful save responses may include warnings when gateways in the environments the agent is exposed to run a version that will not execute the saved forward_client_headers. The warning shape is the existing CompatibilityWarning; this is the first release in which an A2A save can carry one.

One guarantee narrowed without any shape changing, and it is worth reading if you built against the A2A agent's auth_type. Through 1.0.0 the resources-file schema for that field said the gateway's credential is never forwarded from or exposed to the calling client. It now says the credential is not exposed to the caller unless forward_client_headers names the slot auth_type fills, in which case the agent receives the caller's credential in place of the gateway's. The Admin API states the same rule, which it did not describe before this release. The field's shape and enum values are unchanged on both surfaces.

Upgrade Notes

These notes apply when you upgrade from any release earlier than 1.1.0, including an upgrade that passes over it on the way to a later release.

  • Check your etcd: block before upgrading. etcd.dial_timeout_ms and etcd.request_timeout_ms take effect for the first time in this release, and the previous release shipped both of them active at 5000 in config.example.yaml and config.managed.yaml. A configuration file copied from either therefore acquires a live 5-second bound on the configuration range read. A deployment whose configuration set does not load within that bound fails the read and retries it, and — because the gateway now binds its proxy listener only after a successful apply — does not begin serving. Remove request_timeout_ms unless you specifically want a slow etcd to fail fast, and note the trade-off: the unconfirmed-watch abort described under Fixes only takes effect while request_timeout_ms is set. If you keep the key, size it against your own configuration set rather than the number that used to be in the example. dial_timeout_ms: 5000 carries no comparable risk. The shipped examples now leave both keys inactive, and unset means unbounded.
  • Check api.service.nodePort and ui.service.nodePort if your control-plane Helm values file already sets either. Both keys were ignored until this release and are now rendered, so upgrading the chart can move a port that Kubernetes had been allocating dynamically. Remove the key to keep dynamic allocation.