This is an automated email from the ASF dual-hosted git repository.

shreemaan-abhishek pushed a commit to branch chore/release-3.20.0
in repository https://gitbox.apache.org/repos/asf/apisix.git

commit 6b402ee1b0fc091e6b9d72ff2d03e287c9c167d0
Author: Abhishek Choudhary <[email protected]>
AuthorDate: Fri Nov 6 11:50:47 2026 +0545

    chore: release 3.20.0
---
 .requirements                       |  2 +-
 CHANGELOG.md                        | 62 +++++++++++++++++++++++++++++++++++++
 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, 70 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..172898973 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,67 @@ 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 one is now rejected; `appid` (now 
required), `server` and `ssl [...]
+- :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 forwards 
the request only once every requested resource and scope is in the granted 
list, so a route listing several permissions, a permission with several scopes, 
or `http_method_as_scope` on a scoped permission now answers 403 when only part 
of them is granted [#14098](https://github.com/apache/apisix/pull/14098)
+- :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 
summing over `service` reproduces the previous values [#13992](https://git [...]
+- :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 an upstream receives `/api/x` for 
`/admin/..%2Fapi/x` matched on `/api/*`, where it used to receive the raw path. 
A request that differs from `$uri` only by percent-encoding or repeated slashes 
still forwards the original request line, and a plugin that sets 
`$upstream_uri` still takes precedence 
[#14035](https://github.com/apache/apisix/pull/14035)
+- :warning: fix(ssl): match `skip_mtls_uri_regex` against the encoded-slash 
route URI as well, so with `match_uri_encoded_slash: true` the client 
certificate check is skipped only when the exemption 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 
[#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 a client-supplied value no longer reaches the 
upstream under `jwe-decrypt`'s `forward_header` on a `strict: false` route 
without a token, or under `forward-auth`'s `upstream_headers` while the auth 
service is unavailable with `allow_degradation: true` 
[#14096](https://github.com/apache/apisix/pull/14096)
+- :warning: fix(ai-prompt-guard): inspect embeddings string arrays before 
forwarding. A batch such as `{"input": ["secret", "hello"]}` skipped 
inspection; its strings are now joined and checked together, so a configured 
`deny_patterns` entry rejects a batch that was forwarded before. A recognized 
request with no selected text keeps being processed 
[#14073](https://github.com/apache/apisix/pull/14073)
+- :warning: fix(openapi-to-mcp): harden the plugin against a hostile document 
and a hostile caller. 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, so a call can no longer replace a 
credential the Route adds; and a header whose name or value cannot appear in a 
request header is dropped with a warning 
[#13959](https://github.com/apache/apisix/pull/13959)
+  A path parameter is 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 ends up empty, `.` or `..` 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 a different default 
`User-Agent`; set `http_client: lua-resty-http` to keep the previous exchange 
[#14144](https://github.com/apache/apisix/pull/14144)
+
+### 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 
[#14113](https://github.com/apache/apisix/pull/14113)
+- 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 verifying 
registry can no longer reuse a connection another registry opened without 
certificate verification [#14118](https://github.com/apache/apisix/pull/14118)
+- 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): keep secret values out of the `aws` and `vault` error messages, 
which carried the stored secret string and the raw vault response body into 
`error.log` [#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. `core.request.get_body` gains an opt-in 
`opts.decompress`, and the configured size cap bounds the inflated data as well 
as the raw read [#14016](https://github.com/apache/apisix/pull/14016)
+- 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, 
and upgrade `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 
[#13996](https://github.com/apache/apisix/pull/13996)
+- 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 
token that decodes to a JSON primitive is rejected with 401 instead of raising 
a Lua error and 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 growing worker memory on 
every change until the process is OOM-killed 
[#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 
[#14028](https://github.com/apache/apisix/pull/14028)
+- fix(syslog): return when the logger fails to initialize, where the already 
built error fell through to indexing a nil logger and raised a Lua error inside 
the log phase batch processor 
[#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",

  • (apisix) branch chore/relea... shreemaanabhishek.apache.org via notifications
    • (apisix) 01/01: chore:... shreemaanabhishek.apache.org via notifications

Reply via email to