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",

Reply via email to