Docs

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-auth now verifies the token's exp (expiration) and nbf (not-before) claims by default. Previously, a consumer that did not set claims_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 with HTTP 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_verify explicitly in the consumer configuration.

  • HMAC Auth (opens in Plugin Hub docs)

    Upgrade note

    The hmac-auth plugin now defaults signed_headers to ["date"]. After the Data Plane is upgraded, any hmac-auth configuration that does not explicitly set signed_headers requires the client signature to cover the Date header. Clients that were not signing Date will start receiving HTTP 401 with client request can't be validated.

    Before upgrading, ensure your clients sign the Date header, or set signed_headers explicitly in the plugin configuration to match what your clients sign.

  • Batch Requests

    Upgrade note

    The batch-requests plugin now bounds the size of a batch. The number of pipelined sub-requests is limited by a new max_pipeline_items plugin-metadata option (default 1000); a batch that exceeds the limit is rejected with HTTP 400. Pipeline entries that contain fields other than the documented ones are now rejected, and the per-batch timeout must be at least 1 millisecond.

    If you send batches larger than 1000 sub-requests, raise max_pipeline_items in 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:

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-advanced are now built into limit-count. You can configure a sliding window (window_type set to sliding), the redis-sentinel policy, multiple rate-limiting rules (rules), a count derived from a request variable, and shared counters via group and sync_interval. The separate limit-count-advanced plugin remains available for backward compatibility.
  • 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_retries to bound how many fallback retries are attempted, and retry_on_failure_within_ms so 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.
  • AI Prompt Guard (opens in Plugin Hub docs) and the AI content-moderation plugins
    • Added fail_mode (skip, warn, or error, default skip) to control how a consumer-bound plugin handles a request whose format it does not recognize: skip passes the request through, warn passes it through and logs, and error rejects it with HTTP 400.
  • OpenID Connect (opens in Plugin Hub docs)
    • Added support for the lua-resty-session session options (such as cookie_name and cookie_path), so the session cookie can be customized.
    • client_secret is now optional for local JWT verification modes (for example bearer_only with public_key, use_jwks, or private_key_jwt), where a client secret is not required.
  • CAS Auth
    • cas_callback_uri now 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 Vary response header. Responses are cached per variant computed from the headers listed in Vary, and responses with Vary: * are not cached.
  • Kafka Logger (opens in Plugin Hub docs)
    • Added the api_version option (Produce API version 0, 1, or 2). Set it to 2 so that the message timestamp is carried to and stored by the broker; otherwise messages can be recorded without a timestamp.

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_dist and apisix_llm_completion_tokens_dist) and an apisix_llm_latency histogram that includes time-to-first-token (type="ttft").
  • The Prometheus plugin now adds MCP tool dimensions (mcp_tool_name and mcp_request_type) to HTTP metrics, so MCP tools/call traffic 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-Userinfo header that was forwarded upstream. Both plugins now clear any client-supplied X-Userinfo header before authentication, so upstream services only receive plugin-verified identity information.
  • DingTalk Auth
    • Fixed issue: Authentication failures and transient upstream failures were not distinguished. The plugin now returns HTTP 401 for authentication errors and HTTP 503 for transient DingTalk or upstream failures, with clearer error messages.
  • 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 401 otherwise.
    • Fixed issue: A single logout (SLO) POST request with an empty body returned HTTP 500 instead of HTTP 400.
  • 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 permissions were combined with http_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.
  • OPA (opens in Plugin Hub docs)
    • Fixed issue: For a header listed in send_headers_upstream but 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.
  • SAML Auth (opens in Plugin Hub docs)
    • Fixed issue: Reworked plugin loading and error handling — removed the load_resty_saml wrapper, disabled debug output by default, and returns a clean HTTP 500 on authentication errors.
  • AI Proxy (opens in Plugin Hub docs)
    • Fixed issue: An upstream LLM timeout was mapped to HTTP 500. It is now mapped to HTTP 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.
  • 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-Type included a charset parameter was not recognized as a GraphQL request. Content-Type matching is now charset-tolerant.
    • Fixed issue: A PURGE request did not clear all Vary variants of a cached entry. All variants are now purged.
  • 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 nanoid algorithm could produce duplicate or malformed IDs. IDs are now generated with a cryptographically secure random source and always use the valid nanoid alphabet.
  • 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-sentinel policy.
  • Limit Conn (opens in Plugin Hub docs)
    • Fixed issue: A dynamic burst value that resolved to 0 was incorrectly rejected as invalid. It is now allowed.

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: consul service 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: nacos service discovery failed in the stream subsystem because the required shared dictionary was not declared there.

Control Plane

  • Fixed issue: The openid-connect exemption that allows omitting client_secret was 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.