This is an automated email from the ASF dual-hosted git repository.
shreemaan-abhishek pushed a commit to branch release/3.20
in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/release/3.20 by this push:
new 9380e2133 chore: release 3.20.0 (#14168)
9380e2133 is described below
commit 9380e21338d09abf50c23c7f83d51d55458bfcb9
Author: Shreemaan Abhishek <[email protected]>
AuthorDate: Fri Nov 6 18:25:37 2026 +0800
chore: release 3.20.0 (#14168)
---
.requirements | 2 +-
CHANGELOG.md | 63 +++++++++++++++++++++++++++++++++++++
apisix/core/version.lua | 2 +-
docs/en/latest/admin-api.json | 2 +-
docs/en/latest/config.json | 2 +-
docs/en/latest/control-api.json | 4 +--
docs/openapi/admin-api/openapi.yaml | 2 +-
docs/zh/latest/config.json | 2 +-
8 files changed, 71 insertions(+), 8 deletions(-)
diff --git a/.requirements b/.requirements
index 4552899f1..663d155fd 100644
--- a/.requirements
+++ b/.requirements
@@ -18,4 +18,4 @@
APISIX_PACKAGE_NAME=apisix
APISIX_RUNTIME=1.4.2
-APISIX_DASHBOARD_COMMIT=fa2fd0f60f8afffb096476333b9ba63b4c518fa3
+APISIX_DASHBOARD_COMMIT=e5e1457ed972a0dc9634a84ee58bb9aa5a6fa4af
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 03359db9b..e254e90a6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@ title: Changelog
## Table of Contents
+- [3.20.0](#3200)
- [3.19.0](#3190)
- [3.18.0](#3180)
- [3.17.0](#3170)
@@ -87,6 +88,68 @@ title: Changelog
- [0.7.0](#070)
- [0.6.0](#060)
+## 3.20.0
+
+**The changes marked with :warning: are not backward compatible.**
+
+### Change
+
+- :warning: feat!: remove the `mcp-bridge` plugin, which was deprecated in
3.18.0 with a note that it would be removed in a future release. A
configuration that still lists `mcp-bridge` under `plugins` has to drop the
entry, and `openapi-to-mcp` is the MCP plugin going forward
[#14019](https://github.com/apache/apisix/pull/14019)
+- :warning: refactor(wolf-rbac): move `header_prefix` to the Route or Service
configuration, where it defaults to `X-`, so a Route owns its identity headers
and clears `{prefix}UserId`, `{prefix}Username` and `{prefix}Nickname` before
any authenticator runs. Move a custom `header_prefix` from each Consumer to
every Route or Service using `wolf-rbac` (including inside `multi-auth`) before
upgrading, because a Consumer carrying a custom value is now rejected; a
Consumer that only carries t [...]
+- :warning: fix(oas-validator): verify the TLS certificate of the `spec_url`
host. `ssl_verify` now defaults to `true`, so an `https` spec served with a
certificate outside the trust store fails the fetch and the route answers 500;
set `ssl_verify: false` to keep the old behavior, which is now logged as an
opt-out [#14146](https://github.com/apache/apisix/pull/14146)
+- :warning: fix(authz-keycloak): require every requested permission to be
granted. The plugin asks Keycloak for `response_mode=permissions` and checks
each requested resource and scope against the granted list, so a route listing
several permissions, a permission with several scopes, or
`http_method_as_scope` on a scoped permission answers 403 unless all of them
are granted. Each permission is also sent as `resource#scope1,scope2` with
every scope trimmed, so a permission written as `res [...]
+- :warning: feat(openid-connect): upgrade `lua-resty-openidc` to 1.9.3 and
`lua-resty-session` to 4.2.0, so an introspection failure is reported for what
it is: a confirmed inactive token returns 401, an endpoint, transport or
response failure returns 503, and a configuration failure returns 500. An
introspection response whose `active` claim is missing or is not a boolean is
now rejected as an invalid upstream response instead of being read as truthy
[#14088](https://github.com/apache/a [...]
+- :warning: feat(prometheus): add `service` and `service_id` to all four
stream metrics, so `apisix_stream_connection_total`,
`apisix_stream_active_connections`, `apisix_stream_status` and
`apisix_stream_bandwidth` gain two labels. Update PromQL selectors, recording
rules, dashboards and alerts that match those series exactly; both labels are
empty for a session that never reached a Stream Route with a Service, so
aggregating both of them away, for example `sum without (service, service_ [...]
+- :warning: fix(cli): don't capture openresty stdout in `apisix start`. With
`access_log: /dev/stdout`, `apisix start` used to block forever while
swallowing the logs, and it now returns once openresty has daemonized. A
container whose only command is `apisix start` therefore exits, and should run
openresty in the foreground with `daemon off;`, the way the official image
entrypoint does [#14087](https://github.com/apache/apisix/pull/14087)
+- :warning: fix: return 421 for SNI and Host mismatches on mTLS requests, so
an HTTP/2 client that reused a connection opened for another origin can retry
on a new connection with the target origin's SNI and client certificate. The
mismatch check also moves ahead of the client certificate verification result;
other verification failures keep returning 400
[#14013](https://github.com/apache/apisix/pull/14013)
+- :warning: fix(router): forward the path the route was matched on whenever
nginx normalized the request path, so the upstream receives the path the router
matched rather than the raw request line. Because that path is the decoded
`$uri`, an encoded delimiter in a request nginx normalized is forwarded
decoded: `/a/./b%2Fc` reaches the upstream as `/a/b/c`. A request that differs
from `$uri` only by percent-encoding or repeated slashes still forwards the
original request line, and a plugi [...]
+- :warning: fix(ssl): match `skip_mtls_uri_regex` against the encoded-slash
route URI as well, so with `match_uri_encoded_slash: true` an exemption applies
only when it matches both the decoded URI and the URI that keeps `%2F` inside a
path segment. An exemption written for the decoded form alone stops applying to
a path with an encoded slash; extend it to cover the encoded form to keep
skipping the check [#14108](https://github.com/apache/apisix/pull/14108)
+- :warning: fix(jwe-decrypt, forward-auth): clear the forwarded headers on the
paths that skip writing them, so `jwe-decrypt`'s `forward_header` on a `strict:
false` route without a token, and `forward-auth`'s `upstream_headers` while the
auth service is unavailable with `allow_degradation: true`, reach the upstream
unset, the way the normal paths already handle a header they have no value for
[#14096](https://github.com/apache/apisix/pull/14096)
+- :warning: fix(ai-prompt-guard): inspect embeddings string arrays. The
strings of a batch input are joined and checked together, so `allow_patterns`
applies to the combined text and a `deny_patterns` match rejects the batch.
Token-ID arrays are left as they are, and a recognized request with no selected
text keeps being processed [#14073](https://github.com/apache/apisix/pull/14073)
+- :warning: fix(openapi-to-mcp): tighten what a tool call can put into the
upstream request. Tool arguments are filtered to the parameters the operation
declares, so an undeclared header or query key is dropped; the Route's own
`headers` are applied last and take precedence; and a header name or value that
cannot appear in a request header is dropped with a warning. A Route `headers`
entry whose name carries `:` or whitespace, or whose value carries CR or LF, is
now rejected by the schem [...]
+ Path parameters are also confined to one path segment: a value containing
`/` or `\` is rejected unless the Route sets `allow_encoded_path_separators:
true`, and a filled path segment that resolves to an empty, `.` or `..` segment
is rejected [#14154](https://github.com/apache/apisix/pull/14154)
+- :warning: perf(forward-auth): send the authorization request through
`ngx_http_ffi_client`, which is now the default and is selected by
`plugin_attr.forward-auth.http_client`. The auth service sees a `Connection`
header, `Content-Length: 0` on a GET auth request and no default `User-Agent`,
and `timeout` now bounds the whole response read where `lua-resty-http` applied
it to each receive, so a slow auth service can reach `status_on_error` sooner;
set `http_client: lua-resty-http` to ke [...]
+
+### Core
+
+- feat(core): make the readable request header count configurable through
`apisix.max_request_headers_readable`, so plugins that read headers via
`core.request.headers()`, such as `request-validation`, `opa` and
`forward-auth`, can see past the first 100. The default stays `100`, and `0`
reads every header [#14136](https://github.com/apache/apisix/pull/14136)
+- feat(kubernetes): select clusters explicitly with
`discovery_args.cluster_ids`, so an upstream resolves
`namespace/name:port_name` to the union of the matching endpoints of the listed
clusters, cached per cluster endpoint version
[#13991](https://github.com/apache/apisix/pull/13991)
+- feat(consul): support `https` consul server addresses. The scheme is picked
per address, so one `servers` list can mix `http` and `https`, certificates are
verified against `apisix.ssl.ssl_trusted_certificate` by default, and the new
`discovery.consul.ssl_verify` turns verification off for https servers
[#14018](https://github.com/apache/apisix/pull/14018)
+- chore: bump APISIX-Runtime to 1.4.2, which moves OpenResty to 1.31.8.1
(nginx 1.31.8), OpenSSL to 3.5.9, apisix-nginx-module to 1.20.3 and
ngx_multi_upstream_module to 1.4.1. This carries the nginx and OpenSSL security
fixes released since nginx 1.29.2 and OpenSSL 3.4.1, restores the upstream
keepalive wrapping order nginx 1.29.7 changed, fixes `gzip` compression when
the plugin's `buffers.size` is below the page size, fixes 500s on multiplexed
dubbo upstream connections, and adds the [...]
+- chore: bump brotli to 1.2.1 in the CI and dev image install scripts and in
the `brotli` plugin build steps
[#14138](https://github.com/apache/apisix/pull/14138)
+- fix(upstream): verify against `ssl_trusted_certificate` when only
`tls.verify` is set. An upstream with `tls.verify: true` and no `tls.ca_certs`
was verified against an empty trust store and failed with `unable to get local
issuer certificate`; the runtime now loads `*_ssl_trusted_certificate` and the
CRL into the upstream SSL context, and `grpc_ssl_trusted_certificate` is
rendered so grpcs upstreams get the same trust anchors. Because it is rendered
in the server block, a `grpc_ssl_tr [...]
+- fix(router): keep the query string when `normalize_uri_like_servlet` is on,
where every proxied request reached the upstream with the path alone. This also
fixes `proxy-mirror`, which reads the same variable
[#13982](https://github.com/apache/apisix/pull/13982)
+- fix: preserve the `client-control` body limit for gRPC upstreams, where the
redirect to `@grpc_pass` dropped the limit the plugin had set
[#14036](https://github.com/apache/apisix/pull/14036)
+- fix: guard the node version helpers against a resource config that has
already been deleted, which raised an attempt to index a nil value
[#13986](https://github.com/apache/apisix/pull/13986)
+- fix(consul): run the catalog and health watchers as independent timers, so a
watcher is never killed while reading a response body and a whole fetch round
no longer fails with a bogus `resty.http` error when both indexes change
together [#14032](https://github.com/apache/apisix/pull/14032)
+- fix(consul): upgrade `lua-resty-consul` to 0.4.0, whose connect puts the
scheme, SNI and `ssl_verify` into the connection pool name, so a connection is
reused only by a client configured with the same TLS options
[#14118](https://github.com/apache/apisix/pull/14118)
+- fix(discovery): refresh the kubernetes endpoint cache by ttl instead of a
version key, so a worker can no longer keep serving stale pod IPs after an
Endpoints change. The shared dict holds one key per service, written in a
single operation, and a worker re-reads it once its one second cache entry
expires, so a change reaches every worker within a second rather than on the
next request that happens to invalidate the entry. The `tars` discovery used
the same two-key pattern and gets the [...]
+- fix(kubernetes): fail the whole list when a later endpoints page fails, so
the informer re-lists instead of deleting the endpoints of the pages that never
loaded and leaving those services on `no valid upstream node` until a restart
[#14053](https://github.com/apache/apisix/pull/14053)
+- fix(nacos): keep the cached nodes of a service whose refresh failed, where
its last successful snapshot was deleted as if the service had been removed
from the configuration [#13971](https://github.com/apache/apisix/pull/13971)
+- fix(secret): report only identifiers when an `aws` or `vault` reference
cannot be resolved, i.e. the AWS secret name and field and the vault path, so
the provider's content stays out of `error.log`. A vault request rejected with
403 no longer carries the response body in the error either
[#14049](https://github.com/apache/apisix/pull/14049)
+
+### Plugins
+
+- feat: add the `request-decompress` plugin, which inflates a request body
sent with `Content-Encoding: gzip` or `deflate`, so plugins that read the body,
such as `oas-validator`, `request-validation`, `chaitin-waf` and
`body-transformer`, see the plaintext. The upstream receives the inflated body
too, unless `forward_compressed: true`, which restores the original bytes
before proxying, or compresses the body again when another plugin changed it.
`max_req_body_size`, default 1 MiB, bound [...]
+- feat(openid-connect): map verified claims to upstream request headers
through `upstream_headers`, a list of `{header, path}` mappings. Every declared
header is reset on each outcome, so it carries the verified claim or nothing
[#14030](https://github.com/apache/apisix/pull/14030)
+- feat(prometheus): export per-process timers, connection slots and Lua memory
as `apisix_nginx_timers`, `apisix_nginx_timers_max`,
`apisix_nginx_process_connections`, `apisix_nginx_process_connections_max` and
`apisix_nginx_lua_memory_bytes`, so a worker or the privileged agent running
out of any of them is visible before it turns into errors
[#14034](https://github.com/apache/apisix/pull/14034)
+- feat(prometheus): configure the expired metric cleanup interval with
`plugin_attr.prometheus.remove_expired_keys_interval`, default 3600 seconds,
which is also the cap the library applies, while a non-positive value disables
cleanup. The interval is independent of `refresh_interval`. Upgrades
`nginx-lua-prometheus-api7` to 1.1.0
[#13981](https://github.com/apache/apisix/pull/13981)
+- feat(chaitin-waf): add `allow_degradation`, default `false`, so a request
passes through when the WAF service is unavailable or its detection fails,
instead of the service being a single point of failure
[#13311](https://github.com/apache/apisix/pull/13311)
+- feat(limit-count): report the counting window in `$rate_limiting_info`, with
the window type and size, the current window (and the previous window for a
sliding window) in millisecond timestamps, the decision and the cost of the
request. The four existing fields keep their names and values, and the variable
is now encoded from a table, so the field order is no longer fixed and a key
carrying a quote or a backslash no longer produces invalid JSON
[#13996](https://github.com/apache/apisi [...]
+- feat(ai-rate-limiting): expose nested usage fields to `cost_expr` under
names that join the parent and child keys with `__`, for example
`input_tokens_details__cached_tokens`. Top-level fields keep their plain names,
so existing expressions behave the same
[#13984](https://github.com/apache/apisix/pull/13984)
+- fix(limit-count): give each rule of a `rules` configuration its own
delayed-sync queue, timer and limiter, where all rules shared one queue and
synced their keys to Redis through whichever rule's `count` and `time_window`
started the timer [#14064](https://github.com/apache/apisix/pull/14064)
+- fix(limit-count): flush a missing local delta as 0, so a node that only
rejects keeps refreshing its cached quota instead of rejecting on a stale
sliding-window weight for the rest of the window
[#14015](https://github.com/apache/apisix/pull/14015)
+- fix(traffic-split): apply the `tls` and `keepalive_pool` of an inline
`weighted_upstreams[].upstream`, which were both ignored, so a client
certificate is sent, `tls.verify` and `tls.ca_certs` take effect,
`tls.client_cert_id` resolves from that upstream, and the connection pool uses
the configured settings [#14054](https://github.com/apache/apisix/pull/14054)
+- fix(csrf): validate the shape and the fields of the decoded token, so a
malformed token is rejected with 401 instead of answering 500
[#14025](https://github.com/apache/apisix/pull/14025)
+- fix(oas-validator): release a remote spec that no request has used for
`spec_url_ttl` seconds, through the new `inactive` option of `core.lrucache`,
so a route whose `spec_url` changes over time stops accumulating compiled specs
in worker memory [#14061](https://github.com/apache/apisix/pull/14061)
+- fix(ai-proxy): count the Bedrock cache-read and cache-write tokens in
`prompt_tokens` and expose both counters to the LLM logging variables, in
streaming and non-streaming responses
[#14044](https://github.com/apache/apisix/pull/14044)
+- fix(ai-proxy): include the Anthropic cache tokens in prompt usage and
accumulate them across `message_start` and `message_delta`, so prompt caching
no longer reports a cache hit ratio above 100% and no longer undercounts the
tokens that token-based rate limits and metrics are built on. An Anthropic
client reading an OpenAI-compatible upstream also receives `input_tokens` as
`max(0, prompt_tokens - cache_read - cache_write)`, in the buffered response
and in streaming `message_delta`, so [...]
+- fix(syslog): return when the logger fails to initialize, so the failure is
reported as a batch processor error instead of a Lua error in the log phase
[#13970](https://github.com/apache/apisix/pull/13970)
+- fix(syslog): separate the error cause from the message with `": "`, so the
batch processor stops logging run-together text such as `failed to log
messagebuffer overflow` [#13974](https://github.com/apache/apisix/pull/13974)
+- fix(prometheus): bump `nginx-lua-prometheus-api7` to 1.1.1, which updates a
pending metric increment in place instead of allocating a table per `incr()`,
so the latency histograms stop allocating dozens of short-lived tables per
request [#14150](https://github.com/apache/apisix/pull/14150)
+- perf(limit-count): cache the parsed templates of `core.utils.resolve_var` in
an LRU, cache `local` policy limiters by their resolved count, window and
window type, and cache the response header names, so a templated `key` or
`count` stops redoing configuration work on every request. `resolve_var` gains
a `no_cache` argument for templates built per request, which `proxy-rewrite`
passes for a header value carrying `regex_uri` captures
[#14141](https://github.com/apache/apisix/pull/14141)
+- perf(request-id): read only the configured request and response header
instead of building every header, and generate v4 ids through a hex table.
`core.request.set_header(name, value)` and `add_header(name, value)`, the forms
without `ctx`, now also update the cached header table, which a later
`core.request.header(s)` read used to miss
[#14140](https://github.com/apache/apisix/pull/14140)
+
## 3.19.0
**The changes marked with :warning: are not backward compatible.**
diff --git a/apisix/core/version.lua b/apisix/core/version.lua
index 2f6b77207..d28bc0eb9 100644
--- a/apisix/core/version.lua
+++ b/apisix/core/version.lua
@@ -20,5 +20,5 @@
-- @module core.version
return {
- VERSION = "3.19.0"
+ VERSION = "3.20.0"
}
diff --git a/docs/en/latest/admin-api.json b/docs/en/latest/admin-api.json
index 04856a22d..53024c204 100644
--- a/docs/en/latest/admin-api.json
+++ b/docs/en/latest/admin-api.json
@@ -3,7 +3,7 @@
"info": {
"title": "Apache APISIX Admin API",
"description": "The **Apache APISIX Admin API** is a RESTful interface for
managing all APISIX gateway resources — routes, upstreams, services, consumers,
SSL certificates, plugins, and more.\n\nAPISIX Admin API allows you to
dynamically configure the API gateway without restarts. Accepted changes
propagate to workers asynchronously.\n\nUnless API key authentication is
explicitly disabled with `deployment.admin.admin_key_required: false`,
endpoints require an **API key**. Pass a key [...]
- "version": "3.19.0",
+ "version": "3.20.0",
"license": {
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json
index e56d30759..a986b507c 100644
--- a/docs/en/latest/config.json
+++ b/docs/en/latest/config.json
@@ -1,5 +1,5 @@
{
- "version": "3.19.0",
+ "version": "3.20.0",
"sidebar": [
{
"type": "category",
diff --git a/docs/en/latest/control-api.json b/docs/en/latest/control-api.json
index 988be5d6a..6f0e2114a 100644
--- a/docs/en/latest/control-api.json
+++ b/docs/en/latest/control-api.json
@@ -3,7 +3,7 @@
"info": {
"title": "APISIX Control API",
"description": "Use the APISIX Control API to inspect or control the
runtime state of one APISIX instance. It is enabled by default at
`http://127.0.0.1:9090`; configure `apisix.enable_control` and `apisix.control`
in
[`conf/config.yaml`](https://github.com/apache/apisix/blob/master/conf/config.yaml.example).
The API does not provide authentication, so keep it on a loopback or otherwise
trusted internal interface and never expose it to public traffic.\n\nMost
endpoints in this refere [...]
- "version": "3.19.0",
+ "version": "3.20.0",
"license": {
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
@@ -1477,7 +1477,7 @@
"boot_time": 1608522102,
"hostname": "gateway-1",
"id": "b7ce1c5c-b1aa-4df7-888a-cbe403f3e948",
- "version": "3.19.0",
+ "version": "3.20.0",
"etcd_version": "3.5.0"
}
}
diff --git a/docs/openapi/admin-api/openapi.yaml
b/docs/openapi/admin-api/openapi.yaml
index 65cae7c43..ba0bb7b33 100644
--- a/docs/openapi/admin-api/openapi.yaml
+++ b/docs/openapi/admin-api/openapi.yaml
@@ -27,7 +27,7 @@ info:
By default, the Admin API binds to `0.0.0.0:9180` and allows clients from
`127.0.0.0/24`. The examples use `http://127.0.0.1:9180`; configure the
listener and client allowlist in `config.yaml` under
`deployment.admin.admin_listen` and `deployment.admin.allow_admin`.
To learn how to configure and operate APISIX, see [Get
Started](https://apisix.apache.org/docs/apisix/next/getting-started/README/).
- version: 3.19.0
+ version: 3.20.0
license:
name: Apache 2.0
url: https://www.apache.org/licenses/LICENSE-2.0.html
diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json
index 36da7ac93..999731cdb 100644
--- a/docs/zh/latest/config.json
+++ b/docs/zh/latest/config.json
@@ -1,5 +1,5 @@
{
- "version": "3.19.0",
+ "version": "3.20.0",
"sidebar": [
{
"type": "category",