API7 Gateway 3.9.17
max_req_body_bytes and max_resp_body_bytes are now validated as integers of at least 1 in the ClickHouse Logger, Elasticsearch Logger, File Logger, Loggly, Loki Logger, SkyWalking Logger
Release Date: 2026-07-28
Upgrade Notes
Upgrade note — logger body size limits are validated more strictly
max_req_body_bytes and max_resp_body_bytes are now validated as integers of at least 1 in the ClickHouse Logger, Elasticsearch Logger, File Logger, Loggly, Loki Logger, SkyWalking Logger, Alibaba Cloud Logging (SLS), and Syslog plugins. Values that earlier versions accepted — 0, negative numbers, and quoted numbers such as "1024" — are now rejected with HTTP 400.
After the upgrade, existing routes that carry such a value are listed as errors in the gateway group's compatibility report and are not published to the Data Plane; other routes are unaffected. Before upgrading, review these plugins' configurations for max_req_body_bytes or max_resp_body_bytes set to 0, a negative number, or a quoted number, and replace them with a positive integer, or remove the field to fall back to the default of 524288.
Upgrade note — structural Prometheus metric labels can no longer be disabled
The Prometheus (opens in Plugin Hub docs) plugin metadata now rejects disabled_labels entries that would remove a label the metric is built around — for example type on latency, or code on status. Earlier versions accepted these and produced metric series in which distinct measurements collapsed into one.
If your Prometheus plugin metadata disables one of these labels, any update to it after the upgrade is rejected with HTTP 400. Remove the structural entries from disabled_labels before upgrading. Non-structural labels such as route, service, and consumer can still be disabled.
Upgrade note — OpenTelemetry metadata and content moderation deny_code are validated more strictly
Two more schemas now reject values that earlier versions accepted:
- OpenTelemetry (opens in Plugin Hub docs) plugin metadata accepts only scalar values — strings, numbers, and booleans — for
resourceattributes and forcollector.request_headers. Arrays and objects, which earlier versions accepted and then silently dropped at runtime, are now rejected withHTTP 400. - AI Aliyun Content Moderation (opens in Plugin Hub docs) requires
deny_codeto be an integer HTTP status code in the200–599range. AI AWS Content Moderation (opens in Plugin Hub docs) was already validated this way by the Control Plane; this release brings the Data Plane in line.
After the upgrade, an existing configuration that carries such a value is reported as an error in the gateway group's compatibility report, and any update to it is rejected with HTTP 400. Gateway instances stay Healthy and traffic is not affected. Before upgrading, review your OpenTelemetry plugin metadata and any AI Aliyun Content Moderation configuration, and replace or remove the offending values.
Features
Plugins
- OpenID Connect (opens in Plugin Hub docs)
- Added
set_raw_id_token_header. When enabled, the raw ID token issued by the identity provider is forwarded to the upstream service in theX-Raw-ID-Tokenheader, so the upstream can verify the token signature itself instead of trusting the gateway's decoded claims.
- Added
- Proxy Rewrite (opens in Plugin Hub docs)
headers.setandheaders.addnow accept an array of values for a single header name, which is sent upstream as separate header lines rather than one comma-joined line. This matters for upstream services that read only the first occurrence of a header name, or that parse repeated headers differently from a single comma-joined value.
- Kafka Logger (opens in Plugin Hub docs)
- Added
tlsfor connecting to Kafka brokers over TLS, withtls.verifycontrolling broker certificate verification. Previously the plugin could only connect in plaintext, so a broker listening on a TLS port received no logs at all.
- Added
- Rate-limiting and cache plugins
- Added Redis connection keepalive settings (
redis_keepalive_timeoutandredis_keepalive_pool) to the Redis and Redis Cluster policies in Limit Conn (opens in Plugin Hub docs), Limit Req (opens in Plugin Hub docs), and AI Cache (opens in Plugin Hub docs), so operators can tune idle timeout and connection pool size.
- Added Redis connection keepalive settings (
- Body-buffering plugins
- Added
max_req_body_sizeandmax_resp_body_sizeto bound how much of a request or response body a plugin reads into memory, defaulting to 67108864 bytes (64 MiB). A request body larger than the limit is rejected, and a response body is truncated at the limit — except in Proxy Cache, where an oversized response is streamed through without being cached — so one large body cannot exhaust worker memory. Available in AI Proxy (opens in Plugin Hub docs), AI Proxy Multi (opens in Plugin Hub docs), AI Request Rewrite (opens in Plugin Hub docs), the AI prompt plugins, Request Validation (opens in Plugin Hub docs), OAS Validator (opens in Plugin Hub docs), Body Transformer (opens in Plugin Hub docs), Response Rewrite (opens in Plugin Hub docs), Proxy Cache (opens in Plugin Hub docs), gRPC Transcode (opens in Plugin Hub docs), SOAP (opens in Plugin Hub docs), and other plugins that buffer bodies.
- Added
- Logger plugins
- Added
max_req_body_bytesandmax_resp_body_bytesto the remaining logger schemas — ClickHouse Logger (opens in Plugin Hub docs), Elasticsearch Logger (opens in Plugin Hub docs), File Logger, Loggly, Loki Logger (opens in Plugin Hub docs), SkyWalking Logger (opens in Plugin Hub docs), Alibaba Cloud Logging (SLS), and Syslog (opens in Plugin Hub docs) — so the body size limits are validated at configuration time and shown in the Dashboard (see Upgrade Notes).
- Added
Data Plane
- Added
nginx_config.stream.real_ip_from, which lists the addresses trusted to send a PROXY protocol header on stream (TCP/UDP) ports. On a connection from a trusted address, the client address is taken from the PROXY protocol header instead of the directly connected peer, so stream logs and address-based plugins see the real client rather than the load balancer in front of the gateway. It is empty by default and only takes effect on ports that accept the PROXY protocol. - Upgraded the health check engine. Check targets are now reconciled incrementally instead of being destroyed and rebuilt, so scaling an upstream up or down no longer leaves a window in which no node is being checked, and the accumulated health state and failure counters of unchanged nodes are preserved instead of being reset.
- The
Apisix-Pluginsdebug response header now lists the plugins that actually ran, in execution order and annotated with the phase each ran in (for examplelimit-count#access,response-rewrite#header_filter), instead of an unordered list of configured plugins.
Control Plane
- The
/api/fe-configendpoint is now served by the Control Plane binary and driven byconsole.*configuration keys, so the Dashboard's hybrid mode, browser error reporting (Sentry), and a custom sidebar group of external links can be configured through the Control Plane configuration or the Helm chart. Menu items that do not have a name and an absolutehttp/httpsURL are dropped instead of being served to the Dashboard. - Service conflict checks now take route methods into account. Routes on the same host and path that serve disjoint method sets (for example
GET /fooandPOST /foo) are no longer reported as conflicting; an identical method scope is reported as duplicate, and a partially shared scope is reported as overlapping. A route withoutmethodsstill matches all methods. - Added an opt-in SSRF guard for outbound connections to user-configured endpoints. When
security.ssrf_protection.enableis set, the Control Plane refuses to connect to loopback, private, link-local, and carrier-grade NAT addresses — including host names that resolve to them — which prevents features such as service registries and SMTP settings from being used to probe internal services or cloud metadata endpoints.
Console (Dashboard)
- The conflict dialog for services and routes now shows a Methods column, so it is clear on which methods two routes collide. Routes without
methodsare shown as All.
Fixes
Plugins
- AI Proxy (opens in Plugin Hub docs), AI Proxy Multi (opens in Plugin Hub docs), and AI Request Rewrite (opens in Plugin Hub docs)
- Fixed issue: The requests the gateway sent to the LLM provider carried the client's own headers, including
Cookie,Authorization, and arbitrary custom headers, exposing end-user credentials to the upstream provider. The gateway now sends only the headers the plugin itself sets. - Fixed issue: On an error response from the LLM,
$apisix_upstream_response_timeand$llm_time_to_first_tokenwere logged in seconds (for example0.240) or as0, while successful responses were logged in milliseconds. Error-path values are now reported in milliseconds, consistent with the success path.
- Fixed issue: The requests the gateway sent to the LLM provider carried the client's own headers, including
- AI AWS Content Moderation (opens in Plugin Hub docs) and AI Aliyun Content Moderation (opens in Plugin Hub docs)
- Fixed issue:
deny_codeaccepted arbitrary numbers. It is now validated as an integer HTTP status code in the200–599range (default200), so an out-of-range value is rejected at configuration time. Forai-aws-content-moderationthe Control Plane already applied this validation, so this release brings the Data Plane in line; forai-aliyun-content-moderationit now applies on both sides.
- Fixed issue:
- OpenID Connect (opens in Plugin Hub docs)
- Fixed issue: Delivering an authorization callback whose state no longer matched the session — for example after starting a second login flow in the same browser — returned
HTTP 500. The gateway now redirects to the originally requested page instead. - Fixed issue: An empty JSON array in the identity provider's user info (for example
"roles": []) was re-encoded as an empty object ({}) in theX-Userinfoheader on requests served from an existing session, so upstream services that parse the field as an array failed. Empty arrays now stay arrays.
- Fixed issue: Delivering an authorization callback whose state no longer matched the session — for example after starting a second login flow in the same browser — returned
- wolf-rbac
- Fixed issue: When the authentication service returned success without
userInfo, the client's ownX-UserId,X-Username, andX-Nicknameheaders were forwarded to the upstream service, letting a caller present any identity it chose. These headers are now always cleared before the request is proxied.
- Fixed issue: When the authentication service returned success without
- Limit Count (opens in Plugin Hub docs)
- Fixed issue: With
window_type: slidingand delayed synchronization, the remaining quota was reported without the sliding-window weighting, so the gateway admitted noticeably more requests than configured around window boundaries. The remaining count is now window-weighted.
- Fixed issue: With
- Limit Conn (opens in Plugin Hub docs) and Limit Req (opens in Plugin Hub docs)
- Fixed issue: Redis connections were not returned to the keepalive pool, so every request opened a new Redis connection and the configured keepalive settings had no effect. Connections are now pooled and reused.
- Prometheus (opens in Plugin Hub docs)
- Fixed issue: When the shared dictionary used for metrics filled up, the gateway could enter a loop that held a worker at 100% CPU and did not recover after traffic stopped. A full dictionary now degrades gracefully, logging that reported metric data may be incomplete.
- OpenTelemetry (opens in Plugin Hub docs)
- Fixed issue: The plugin metadata schema accepted non-scalar values for
resourceattributes andcollector.request_headers, which were then silently dropped at runtime. Such values are now rejected at configuration time.
- Fixed issue: The plugin metadata schema accepted non-scalar values for
Data Plane
- Fixed issue: With the
least_connload balancer, adding or removing an upstream node discarded the tracked connection counts, so the balancer degraded to round robin and sent new requests to nodes that were already holding long-lived connections. Load state is now preserved across upstream scaling. - Fixed issue: When a plugin field referenced a secret that could not be resolved — an unset environment variable, or an error from the secret manager — the failure was silent and the unresolved reference was used as the literal value. The gateway now logs an error identifying the reference and the field it appears in.
- Fixed issue: The gateway read its host name by executing
/bin/hostname, so on images that do not ship that binary it reported no host name and gateway instances appeared without a host name in the Dashboard. The host name is now read through a system call. - Fixed issue: Entries in
nginx_config.envswhose values contained spaces, quotes, or backslashes produced an invalid NGINX configuration and the gateway failed to start. These values are now quoted and escaped correctly. - Fixed issue: Stopping and immediately restarting the gateway through the CLI could fail because the previous instance had not finished exiting. The CLI now waits for it to stop before starting the new one.
- Fixed issue: After a gateway container was killed rather than shut down cleanly, leftover worker event sockets could prevent the next start from binding. They are now removed during startup.
- Fixed issue: On arm64, a dependency pulled a second copy of the JSON library into the gateway's module path, shadowing the bundled one and encoding empty arrays as invalid JSON. The redundant dependency is removed, so the bundled library is always used.
Control Plane
- Fixed issue: Two concurrent PATCH requests against the same resource could each read the resource before the other wrote it, so only the last write survived even though both requests reported success. PATCH requests on the same resource are now serialized.
- Fixed issue: Pooled database connections were reused indefinitely, so after a database failover the Control Plane could keep using connections pinned to a demoted, now read-only primary. Connection lifetime is now bounded at one hour by default and can be tuned with
database.max_lifetime. - Fixed issue: Importing a license whose certificate chain had expired reported
license certificate comes from an invalid issuer, pointing at the signing authority rather than at the expiry. An expired or not-yet-effective certificate chain now reports a dedicated message that includes the relevant timestamp. Certificates from a genuinely unknown authority still report an invalid issuer. - Fixed issue: The current data plane core count included instances that had stopped sending heartbeats, which stay in LostConnection for up to two hours before being marked offline, so the Dashboard's core usage remained inflated long after a data plane was scaled down or crashed. Only connected instances in standard running mode are counted now. License accounting, which is computed from heartbeat usage, is unchanged.
Console (Dashboard)
- Fixed issue: The route paths and service hosts forms allowed adding entries beyond the schema limits of 64 paths and 32 hosts. Creating such a resource failed with a raw server error, and editing one could store a configuration that
adc validateandadc synclater rejected. The Add control is now hidden once a list reaches its limit, while lists that are already over the limit still show every entry so the excess can be removed.