kayx23 commented on code in PR #14168: URL: https://github.com/apache/apisix/pull/14168#discussion_r4490089579
########## CHANGELOG.md: ########## @@ -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_verify` stay on the Consumer [#13929](https://github.com/apache/apisix/pull/13929) +- :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`, which is the form Keycloak splits on [#14098](https://github.com/apache/apisix/pull/14098) Review Comment: The all-or-nothing grant check is right, but the permission rewrite also changes which scopes an existing configuration asks for. `parse_permission` trims each scope, then the request is sent as `resource#scope1,scope2` with no spaces (`apisix/plugins/authz-keycloak.lua`). A permission written as `res#view, delete` used to ask Keycloak for the scope `" delete"`, which Keycloak dropped; `delete` is now requested and must be granted, or the route returns 403 (or the `access_denied_redirect_uri` redirect). Please say that. A token-endpoint body that is not a permissions list is also rejected with 503 (`Could not decode JSON from token endpoint`) instead of being treated as an allow under the old `response_mode=decision` path. ########## CHANGELOG.md: ########## @@ -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_verify` stay on the Consumer [#13929](https://github.com/apache/apisix/pull/13929) +- :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`, which is the form Keycloak splits on [#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/apisix/pull/14088) +- :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://github.com/apache/apisix/pull/13992) +- :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. 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) Review Comment: The exception is right: a request that differs from `$uri` only by percent-encoding or repeated slashes still forwards the original request line (`uri_normalized_by_nginx` in `apisix/init.lua`). The case that does rewrite needs its consequence. Once nginx has done more than decode the path or merge slashes (a dot segment, including an encoded one, or a `#`), `ngx.req.set_uri($uri)` runs and encoded delimiters in that path are forwarded decoded. `/a/./b%2Fc` reaches the upstream as `/a/b/c`. There is no switch back to the raw request line. ########## CHANGELOG.md: ########## @@ -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_verify` stay on the Consumer [#13929](https://github.com/apache/apisix/pull/13929) +- :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`, which is the form Keycloak splits on [#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/apisix/pull/14088) +- :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://github.com/apache/apisix/pull/13992) +- :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. 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` 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 [#13959](https://github.com/apache/apisix/pull/13959) Review Comment: Two upgrade effects on this line are still missing, separate from the response/document limits and Origin/session binding. Route `headers` are now rejected by the schema when the name contains `:` or whitespace, or the value contains CR or LF (`openapi-to-mcp.lua` `patternProperties`). A stored route with such a header fails to load; that is not the same as dropping a bad header from a tool call. `#14154` also strips a `#` fragment from the operation template before the upstream request (`without_fragment` / `uri_path` in `tools/handler.lua`). A template that carried a fragment no longer sends it. ########## CHANGELOG.md: ########## @@ -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_verify` stay on the Consumer [#13929](https://github.com/apache/apisix/pull/13929) +- :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`, which is the form Keycloak splits on [#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/apisix/pull/14088) Review Comment: The 401/503/500 split and the non-boolean `active` claim match `lua-resty-openidc` 1.9.3. The rockspec jump is from 1.9.0 / `lua-resty-session` 4.1.5, so 1.9.1 is part of this upgrade and is not described here: session-destroy and closed-mode revocation-store failures are now propagated from logout, session initialization, and access-token retrieval, and logout is not redirected when the session cannot be destroyed. `openid-connect` reaches that path through `openidc.authenticate` on the authorization-code flow. The plugin docs also note that concurrent introspection cache misses are coalesced, and that a coordination timeout falls back to direct introspection. That belongs in this entry if it is listing what the upgrade changes for callers. ########## CHANGELOG.md: ########## @@ -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_verify` stay on the Consumer [#13929](https://github.com/apache/apisix/pull/13929) +- :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`, which is the form Keycloak splits on [#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/apisix/pull/14088) +- :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://github.com/apache/apisix/pull/13992) +- :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. 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` 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 [#13959](https://github.com/apache/apisix/pull/13959) + 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 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 post-quantum hybrid group `X25519MLKEM768` to the default TLS 1.3 key shares [#14041](https://github.com/apache/apisix/pull/14041) [#14126](https://github.com/apache/apisix/pull/14126) +- 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 connection is reused only by a client configured with the same TLS options [#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): 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. `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) Review Comment: The upstream receives the decompressed body by default, not only the later plugins. `forward_compressed` defaults to `false`; when it is `true`, plugins still read plaintext and the compressed bytes are restored in `before_proxy` (a body another plugin rewrote is deflated again). The cap is `max_req_body_size`, default 1048576. Rejections are 415 with `Accept-Encoding: gzip, deflate` for an unsupported coding, 400 for a malformed body, and 413 when the inflated body exceeds the cap. On a route that also enables `hmac-auth` with `validate_request_body`, the digest is over the inflated body unless that plugin is moved above priority 2850. ########## CHANGELOG.md: ########## @@ -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_verify` stay on the Consumer [#13929](https://github.com/apache/apisix/pull/13929) +- :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`, which is the form Keycloak splits on [#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/apisix/pull/14088) +- :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://github.com/apache/apisix/pull/13992) +- :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. 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` 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 [#13959](https://github.com/apache/apisix/pull/13959) + 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 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 post-quantum hybrid group `X25519MLKEM768` to the default TLS 1.3 key shares [#14041](https://github.com/apache/apisix/pull/14041) [#14126](https://github.com/apache/apisix/pull/14126) +- 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 connection is reused only by a client configured with the same TLS options [#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): 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. `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) Review Comment: The default of 3600 is right, and two bounds are missing. The library caps any larger value at 3600, and a non-positive value disables cleanup. The interval is independent of `refresh_interval`. After it is shortened on reload, the next reclamation can still wait for the previous interval to elapse, because the workers share one marker. See `docs/en/latest/plugins/prometheus.md`. ########## CHANGELOG.md: ########## @@ -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_verify` stay on the Consumer [#13929](https://github.com/apache/apisix/pull/13929) +- :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`, which is the form Keycloak splits on [#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/apisix/pull/14088) +- :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://github.com/apache/apisix/pull/13992) +- :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. 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` 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 [#13959](https://github.com/apache/apisix/pull/13959) + 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 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 post-quantum hybrid group `X25519MLKEM768` to the default TLS 1.3 key shares [#14041](https://github.com/apache/apisix/pull/14041) [#14126](https://github.com/apache/apisix/pull/14126) +- 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 connection is reused only by a client configured with the same TLS options [#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): 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. `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 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) Review Comment: Idle `spec_url` entries are released after `spec_url_ttl`, as this says. The same PR also drops the strong reference `core.lrucache` kept to every cache it created (`stale_obj_pool` and the inactive registry are weak-keyed). Changing `spec_url_ttl` in the plugin metadata rebuilds the cache; before this, the previous cache and every compiled spec in it stayed reachable for the life of the worker. ########## CHANGELOG.md: ########## @@ -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_verify` stay on the Consumer [#13929](https://github.com/apache/apisix/pull/13929) +- :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`, which is the form Keycloak splits on [#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/apisix/pull/14088) +- :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://github.com/apache/apisix/pull/13992) +- :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. 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` 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 [#13959](https://github.com/apache/apisix/pull/13959) + 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 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 post-quantum hybrid group `X25519MLKEM768` to the default TLS 1.3 key shares [#14041](https://github.com/apache/apisix/pull/14041) [#14126](https://github.com/apache/apisix/pull/14126) +- 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 connection is reused only by a client configured with the same TLS options [#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): 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. `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) Review Comment: This PR also changes existing output. The key is passed through `core.json.encode`, so a quote or backslash in a header-based key no longer makes `$rate_limiting_info` invalid JSON. The local fixed-window end time is stored with `dict:add` and kept when the key already exists (`limit-count-local.lua` `set_endtime`). Previously the end time was rewritten whenever the counter equaled the request cost, so an `ai-rate-limiting` zero-cost check, and the first real charge after it, pushed `X-AI-RateLimit-Reset` later while the counter kept its original TTL. ########## CHANGELOG.md: ########## @@ -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_verify` stay on the Consumer [#13929](https://github.com/apache/apisix/pull/13929) +- :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`, which is the form Keycloak splits on [#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/apisix/pull/14088) +- :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://github.com/apache/apisix/pull/13992) +- :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. 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` 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 [#13959](https://github.com/apache/apisix/pull/13959) + 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 a different default `User-Agent`; set `http_client: lua-resty-http` to keep the previous exchange [#14144](https://github.com/apache/apisix/pull/14144) Review Comment: Two other client differences change the outcome, not just the framing. `timeout` is one budget for the whole response read; `lua-resty-http` applied it on each receive, so a slow or chunked auth response can now return `status_on_error` sooner (`docs/en/latest/plugins/forward-auth.md`). An empty `Host` supplied through `extra_headers` is replaced with the address the FFI client dials (`t/plugin/forward-auth4.t` TEST 19, `127.0.0.1:1986`). `lua-resty-http` still sends that empty `Host`, and an nginx auth service answers 400 (TEST 21). ########## CHANGELOG.md: ########## @@ -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_verify` stay on the Consumer [#13929](https://github.com/apache/apisix/pull/13929) +- :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`, which is the form Keycloak splits on [#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/apisix/pull/14088) +- :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://github.com/apache/apisix/pull/13992) +- :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. 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` 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 [#13959](https://github.com/apache/apisix/pull/13959) + 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 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 post-quantum hybrid group `X25519MLKEM768` to the default TLS 1.3 key shares [#14041](https://github.com/apache/apisix/pull/14041) [#14126](https://github.com/apache/apisix/pull/14126) +- 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) Review Comment: Rendering `grpc_ssl_trusted_certificate` from `ssl.ssl_trusted_certificate` is new, and it is emitted in the server block (`apisix/cli/ngx_tpl.lua`, next to the existing `proxy_ssl_trusted_certificate`). That overrides the same directive set in `http_configuration_snippet`. A deployment that pointed gRPC at a different CA from that snippet has to add the CA to `ssl_trusted_certificate`. `proxy_ssl_trusted_certificate` was already rendered, so this override is new for gRPC only. ########## CHANGELOG.md: ########## @@ -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_verify` stay on the Consumer [#13929](https://github.com/apache/apisix/pull/13929) +- :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`, which is the form Keycloak splits on [#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/apisix/pull/14088) +- :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://github.com/apache/apisix/pull/13992) +- :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. 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` 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 [#13959](https://github.com/apache/apisix/pull/13959) + 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 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 post-quantum hybrid group `X25519MLKEM768` to the default TLS 1.3 key shares [#14041](https://github.com/apache/apisix/pull/14041) [#14126](https://github.com/apache/apisix/pull/14126) +- 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 connection is reused only by a client configured with the same TLS options [#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): 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. `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 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 [#14028](https://github.com/apache/apisix/pull/14028) Review Comment: Native Anthropic `$llm_prompt_tokens` is covered. The same PR changes the body an Anthropic client receives from an OpenAI-compatible upstream: `input_tokens` is `max(0, prompt_tokens - cache_read - cache_write)`, so cache writes are no longer counted twice, for both the buffered response and streaming `message_delta` (`anthropic-messages-to-openai-chat.lua`). `$llm_cache_creation_input_tokens` is also filled when the upstream reports the cache write only under `prompt_tokens_details`, where it used to stay 0. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
