API7 Gateway 3.9.14
JWT Auth — jwt-auth now verifies the token's exp (expiration) and nbf (not-before) claims by default.
Release Date: 2026-06-15
Breaking Changes
Plugins
-
JWT Auth (opens in Plugin Hub docs)
Upgrade note
jwt-authnow verifies the token'sexp(expiration) andnbf(not-before) claims by default. Previously, a consumer that did not setclaims_to_verify(or set it to an empty list) accepted any correctly signed token, including expired ones. After the Data Plane is upgraded, such tokens are rejected withHTTP 401.If you relied on expired tokens being accepted, account for this behavior change before upgrading. To keep verifying only specific claims, set
claims_to_verifyexplicitly in the consumer configuration. -
HMAC Auth (opens in Plugin Hub docs)
Upgrade note
The
hmac-authplugin now defaultssigned_headersto["date"]. After the Data Plane is upgraded, anyhmac-authconfiguration that does not explicitly setsigned_headersrequires the client signature to cover theDateheader. Clients that were not signingDatewill start receivingHTTP 401withclient request can't be validated.Before upgrading, ensure your clients sign the
Dateheader, or setsigned_headersexplicitly in the plugin configuration to match what your clients sign. -
Batch Requests
Upgrade note
The
batch-requestsplugin now bounds the size of a batch. The number of pipelined sub-requests is limited by a newmax_pipeline_itemsplugin-metadata option (default1000); a batch that exceeds the limit is rejected withHTTP 400. Pipeline entries that contain fields other than the documented ones are now rejected, and the per-batchtimeoutmust be at least1millisecond.If you send batches larger than 1000 sub-requests, raise
max_pipeline_itemsin the plugin metadata. If your clients send entries with undocumented fields, remove them before upgrading.
Developer Portal
-
Developer authentication is enforced only for published API products
Upgrade note
Previously, API products in draft state also had their developer authentication rules synced to the gateway, so routes belonging to a draft product required developer authentication. Starting in 3.9.14, only published API products contribute developer-authentication rules to the data plane.
After upgrading, routes that belong to a draft (unpublished) API product no longer require developer authentication until the product is published. If you relied on draft products being protected, publish them or restrict access another way.
Upgrade Notes
Upgrade note — default request body size limits
The forward-auth, ai-proxy, and ai-proxy-multi plugins now enforce a max_req_body_size limit when they read the request body (default 64 MB). A request whose body exceeds the limit is rejected with HTTP 413 Request Entity Too Large.
If you proxy large request bodies through these plugins, set max_req_body_size explicitly to a value that fits your workload before upgrading.
Upgrade note — additional plugin secret fields are encrypted at rest
The Control Plane now encrypts additional credential-bearing plugin fields at rest. Because API7 EE upgrades the Control Plane before the Data Plane, during the upgrade window a 3.9.14 Control Plane encrypts these fields while an older 3.9.13 Data Plane cannot decrypt them, which can cause the affected plugins to fail until the Data Plane is also upgraded.
The newly encrypted fields, by plugin, are:
- AI AWS Content Moderation (opens in Plugin Hub docs):
comprehend.secret_access_key - Azure Functions:
master_apikey,authorization.apikey - DingTalk Auth:
secret_fallbacks - Error Log Logger (opens in Plugin Hub docs):
kafka.brokers.sasl_config.password - Feishu Auth:
secret_fallbacks - HTTP Logger (opens in Plugin Hub docs):
auth_header - Kafka Logger (opens in Plugin Hub docs):
brokers.sasl_config.password - Loggly:
customer_token - OpenFunction:
authorization.service_token - OpenID Connect (opens in Plugin Hub docs):
session.secret - Splunk HEC Logging (opens in Plugin Hub docs):
endpoint.token
If you use any of these plugins with the listed fields, upgrade the Data Plane to 3.9.14 promptly after the Control Plane, and avoid editing those plugins until both sides are on 3.9.14.
Features
Plugins
- Limit Count (opens in Plugin Hub docs)
- The advanced rate-limiting capabilities previously provided by
limit-count-advancedare now built intolimit-count. You can configure a sliding window (window_typeset tosliding), theredis-sentinelpolicy, multiple rate-limiting rules (rules), acountderived from a request variable, and shared counters viagroupandsync_interval. The separatelimit-count-advancedplugin remains available for backward compatibility.
- The advanced rate-limiting capabilities previously provided by
- AI Proxy (opens in Plugin Hub docs)
- The upstream LLM request body is now JSON-encoded with sorted keys. This produces a stable, byte-identical body for equivalent requests, which improves prompt-cache hit rates on LLM providers that cache by exact request payload.
- AI Proxy Multi (opens in Plugin Hub docs)
- Added
max_retriesto bound how many fallback retries are attempted, andretry_on_failure_within_msso that only a failure occurring within the configured window triggers a fallback. A slow failure is returned to the client directly instead of multiplying the total wait time across fallback instances.
- Added
- AI Prompt Guard (opens in Plugin Hub docs) and the AI content-moderation plugins
- Added
fail_mode(skip,warn, orerror, defaultskip) to control how a consumer-bound plugin handles a request whose format it does not recognize:skippasses the request through,warnpasses it through and logs, anderrorrejects it withHTTP 400.
- Added
- OpenID Connect (opens in Plugin Hub docs)
- Added support for the
lua-resty-sessionsession options (such ascookie_nameandcookie_path), so the session cookie can be customized. client_secretis now optional for local JWT verification modes (for examplebearer_onlywithpublic_key,use_jwks, orprivate_key_jwt), where a client secret is not required.
- Added support for the
- CAS Auth
cas_callback_urinow accepts an absolute URL, which is used as-is as the CAS service URL. This is useful when the gateway sits behind a proxy and the externally visible callback URL differs from the request path.
- Proxy Cache (opens in Plugin Hub docs)
- The in-memory cache strategy now honors the
Varyresponse header. Responses are cached per variant computed from the headers listed inVary, and responses withVary: *are not cached.
- The in-memory cache strategy now honors the
- Kafka Logger (opens in Plugin Hub docs)
- Added the
api_versionoption (Produce API version0,1, or2). Set it to2so that the message timestamp is carried to and stored by the broker; otherwise messages can be recorded without a timestamp.
- Added the
Data Plane
- Added built-in NGINX variables for AI requests (
$llm_model,$request_llm_model,$llm_prompt_tokens,$llm_completion_tokens,$llm_total_tokens,$llm_time_to_first_token,$llm_stream) that can be referenced in the NGINX access log format to record the per-request LLM model and token usage. - The Prometheus plugin now exports LLM token-distribution histograms (
apisix_llm_prompt_tokens_distandapisix_llm_completion_tokens_dist) and anapisix_llm_latencyhistogram that includes time-to-first-token (type="ttft"). - The Prometheus plugin now adds MCP tool dimensions (
mcp_tool_nameandmcp_request_type) to HTTP metrics, so MCPtools/calltraffic can be broken down by tool and request type.
Developer Portal
- Added an Approvals workflow for the Developer Portal. Platform admins can review and accept or reject API-product subscription and developer-registration requests from the portal, with the applicant's organization name resolved for display. The acting administrator is recorded as the operator for auditing, and the Dashboard shows the resolved operator.
- Credential secrets (such as key-auth and basic-auth keys) are now returned only once, at creation time. Subsequent reads of the credential omit the secret value.
- Added an admin Users page to manage portal users — list, search, change role, ban or unban, and delete.
- Added two-factor authentication for portal sign-in.
- Added policy-based SSO sign-in: developers can be routed to a configured SSO provider based on their email domain, including anchored, case-insensitive regular-expression matching.
- Added a configurable Terms of Service acceptance step during sign-up.
- Added dark mode.
Fixes
Plugins
- Error Page (opens in Plugin Hub docs)
- Fixed issue: The plugin decided whether to render a custom error page based on the upstream status variable, which could replace error responses that actually came from the upstream service. It now classifies the response source — custom error pages are rendered only for errors generated by the gateway or plugins, while genuine error responses returned by the upstream are passed through unchanged.
- Feishu Auth and DingTalk Auth
- Fixed issue: A client could supply a forged
X-Userinfoheader that was forwarded upstream. Both plugins now clear any client-suppliedX-Userinfoheader before authentication, so upstream services only receive plugin-verified identity information.
- Fixed issue: A client could supply a forged
- DingTalk Auth
- Fixed issue: Authentication failures and transient upstream failures were not distinguished. The plugin now returns
HTTP 401for authentication errors andHTTP 503for transient DingTalk or upstream failures, with clearer error messages.
- Fixed issue: Authentication failures and transient upstream failures were not distinguished. The plugin now returns
- CAS Auth
- Fixed issue: The login callback did not validate the signed initiation cookie, allowing a crafted callback request to drive the post-login redirect. The callback now requires a valid signed initiation cookie and is rejected with
HTTP 401otherwise. - Fixed issue: A single logout (SLO)
POSTrequest with an empty body returnedHTTP 500instead ofHTTP 400.
- Fixed issue: The login callback did not validate the signed initiation cookie, allowing a crafted callback request to drive the post-login redirect. The callback now requires a valid signed initiation cookie and is rejected with
- Authz Casdoor
- Fixed issue: The session cookie name was shared across Casdoor clients, so sessions for different clients could collide. The session cookie is now scoped per client.
- Fixed issue: The gateway session was not tied to the Casdoor token lifetime, so it could continue to be reused after the token expired. The session now expires when the Casdoor token expires, forcing re-authentication.
- Authz Keycloak (opens in Plugin Hub docs)
- Fixed issue: When static
permissionswere combined withhttp_method_as_scope, the derived method scope was written back to the reused plugin configuration, causing scopes to accumulate across requests. The permission list is now cloned before the method scope is appended.
- Fixed issue: When static
- OPA (opens in Plugin Hub docs)
- Fixed issue: For a header listed in
send_headers_upstreambut absent from the OPA response, a client-supplied value could be forwarded upstream. Such headers are now cleared so only OPA-provided values reach the upstream.
- Fixed issue: For a header listed in
- SAML Auth (opens in Plugin Hub docs)
- Fixed issue: Reworked plugin loading and error handling — removed the
load_resty_samlwrapper, disabled debug output by default, and returns a cleanHTTP 500on authentication errors.
- Fixed issue: Reworked plugin loading and error handling — removed the
- AI Proxy (opens in Plugin Hub docs)
- Fixed issue: An upstream LLM timeout was mapped to
HTTP 500. It is now mapped toHTTP 504 Gateway Time-out. - Fixed issue: In passthrough mode, the client's HTTP method and query string were not forwarded to the upstream. They are now preserved.
- Fixed issue: An upstream LLM timeout was mapped to
- AI Proxy Multi (opens in Plugin Hub docs)
- Fixed issue: Health checks for domain-based upstreams could be unstable — a cached node picker could go stale after health checkers were created, and the health-check configuration could be mutated in place across requests.
- GraphQL Limit Count (opens in Plugin Hub docs)
- Fixed issue: The query nesting depth was miscalculated, making depth-based rate limiting inaccurate. Depth is now computed from the true maximum nesting depth with fragment expansion, and Content-Type matching tolerates a charset parameter.
- GraphQL Proxy Cache (opens in Plugin Hub docs)
- Fixed issue: A request whose
Content-Typeincluded a charset parameter was not recognized as a GraphQL request. Content-Type matching is now charset-tolerant. - Fixed issue: A
PURGErequest did not clear allVaryvariants of a cached entry. All variants are now purged.
- Fixed issue: A request whose
- AWS Lambda (opens in Plugin Hub docs)
- Fixed issue: IAM (SigV4) authentication failed for a request with URL-encoded or multi-value query parameters because the canonical query string was computed incorrectly.
- AWS Secret Manager
- Fixed issue: Resolving a secret whose name contains a slash failed. Such names are now parsed correctly.
- Request ID (opens in Plugin Hub docs)
- Fixed issue: The
nanoidalgorithm could produce duplicate or malformed IDs. IDs are now generated with a cryptographically secure random source and always use the valid nanoid alphabet.
- Fixed issue: The
- Proxy Mirror (opens in Plugin Hub docs)
- Fixed issue: When mirroring a gRPC request, the original method path was not preserved on the mirrored request. It is now kept intact.
- Body Transformer (opens in Plugin Hub docs)
- Fixed issue: XML-to-JSON transformation intermittently lost keys that used an XML namespace prefix. Namespaced keys are now preserved and accessible in the template.
- Elasticsearch Logger (opens in Plugin Hub docs)
- Fixed issue: A dynamic index template using date placeholders could raise an error for an invalid template. The date formatting is now guarded.
- Rate limiting (Limit Count, Limit Req, Limit Conn (opens in Plugin Hub docs))
- Fixed issue: Redis connections that differed only by database number or credentials could share the same keepalive connection pool, so a connection could be reused against the wrong database or identity. Connections are now isolated by database, credentials, and TLS settings; this also covers the
redis-sentinelpolicy.
- Fixed issue: Redis connections that differed only by database number or credentials could share the same keepalive connection pool, so a connection could be reused against the wrong database or identity. Connections are now isolated by database, credentials, and TLS settings; this also covers the
- Limit Conn (opens in Plugin Hub docs)
- Fixed issue: A dynamic
burstvalue that resolved to0was incorrectly rejected as invalid. It is now allowed.
- Fixed issue: A dynamic
Data Plane
- Fixed issue: When multiple logging plugins captured the response body on the same request, their response-body buffers could interfere with each other, producing truncated or mixed log output. Each logger now uses an isolated buffer.
- Fixed issue: Some logger plugins wrote debug logs that could expose credentials. These logs have been removed.
- Fixed issue: A logging plugin in the log phase could fail when the access phase had been short-circuited, for example by an authentication rejection.
- Fixed issue: A cached request header could keep a stale value when a plugin set the same header using a different letter case. Header cache keys are now normalized.
- Fixed issue:
consulservice discovery discarded all remaining nodes of a service when a single node entry was invalid. Invalid node entries are now skipped individually so the remaining healthy nodes are still used. - Fixed issue:
nacosservice discovery failed in the stream subsystem because the required shared dictionary was not declared there.
Control Plane
- Fixed issue: The
openid-connectexemption that allows omittingclient_secretwas applied too broadly. It is now scoped to the OIDC flows that genuinely do not require a client secret. - Fixed issue: Listing labels scoped to a gateway group returned the global labels for the resource type instead of the gateway-group-scoped labels.
- Fixed issue: Developer Portal organization invitations did not honor the configured email-verification requirement.