This is an automated email from the ASF dual-hosted git repository.
nic-6443 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push:
new cb72d0eb6 fix(ai-proxy-multi): keep existing query string in health
check path; cover construct_upstream mutation contract (#13506)
cb72d0eb6 is described below
commit cb72d0eb6276f3c68a7037e3f6bec7301124049a
Author: Nic <[email protected]>
AuthorDate: Thu Jun 11 10:35:52 2026 +0800
fix(ai-proxy-multi): keep existing query string in health check path; cover
construct_upstream mutation contract (#13506)
---
apisix/plugins/ai-proxy-multi.lua | 6 +-
t/plugin/ai-proxy-multi-construct-upstream.t | 166 +++++++++++++++++++++++++++
2 files changed, 170 insertions(+), 2 deletions(-)
diff --git a/apisix/plugins/ai-proxy-multi.lua
b/apisix/plugins/ai-proxy-multi.lua
index 05cfc46ce..13a4b8e3e 100644
--- a/apisix/plugins/ai-proxy-multi.lua
+++ b/apisix/plugins/ai-proxy-multi.lua
@@ -630,8 +630,10 @@ function _M.construct_upstream(instance)
end
end
if auth.query then
- checks.active.http_path = string.format("%s?%s",
- checks.active.http_path,
core.string.encode_args(auth.query))
+ local http_path = checks.active.http_path or "/"
+ local sep = string.find(http_path, "?", 1, true) and "&" or "?"
+ checks.active.http_path = http_path .. sep ..
+ core.string.encode_args(auth.query)
end
end
upstream.nodes = upstream_nodes
diff --git a/t/plugin/ai-proxy-multi-construct-upstream.t
b/t/plugin/ai-proxy-multi-construct-upstream.t
new file mode 100644
index 000000000..96f1af1f2
--- /dev/null
+++ b/t/plugin/ai-proxy-multi-construct-upstream.t
@@ -0,0 +1,166 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+use t::APISIX 'no_plan';
+
+log_level("info");
+repeat_each(1);
+no_long_string();
+no_shuffle();
+no_root_location();
+
+
+add_block_preprocessor(sub {
+ my ($block) = @_;
+
+ if (!defined $block->request) {
+ $block->set_value("request", "GET /t");
+ }
+
+ my $user_yaml_config = <<_EOC_;
+plugins:
+ - ai-proxy-multi
+_EOC_
+ $block->set_value("extra_yaml_config", $user_yaml_config);
+});
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: construct_upstream must not mutate the instance checks in place
+# The healthcheck manager calls construct_upstream once per second per
+# instance from its timers, always passing the cached route config. The
+# returned checks must carry the auth header/query, but the input table must
+# stay untouched: otherwise auth.query is appended to checks.active.http_path
+# again on every call, and the cached config no longer matches the config
+# delivered by the config center.
+--- config
+ location /t {
+ content_by_lua_block {
+ local plugin = require("apisix.plugins.ai-proxy-multi")
+ local instance = {
+ name = "ins",
+ provider = "openai",
+ weight = 1,
+ auth = {
+ header = {
+ Authorization = "Bearer token",
+ },
+ query = {
+ api_key = "secret",
+ },
+ },
+ options = {
+ model = "gpt-4",
+ },
+ override = {
+ endpoint = "http://127.0.0.1:16724",
+ },
+ checks = {
+ active = {
+ type = "http",
+ http_path = "/status",
+ healthy = {
+ interval = 1,
+ successes = 1,
+ },
+ unhealthy = {
+ interval = 1,
+ http_failures = 2,
+ },
+ },
+ },
+ }
+
+ for i = 1, 3 do
+ local upstream, err = plugin.construct_upstream(instance)
+ assert(upstream, err)
+ assert(upstream.checks.active.http_path ==
"/status?api_key=secret",
+ "call " .. i .. ": unexpected http_path: "
+ .. upstream.checks.active.http_path)
+ local req_headers = upstream.checks.active.req_headers
+ assert(#req_headers == 1 and req_headers[1] == "Authorization:
Bearer token",
+ "call " .. i .. ": unexpected req_headers: "
+ .. require("cjson.safe").encode(req_headers))
+ end
+
+ assert(instance.checks.active.http_path == "/status",
+ "instance checks.active.http_path mutated in place: "
+ .. instance.checks.active.http_path)
+ assert(instance.checks.active.req_headers == nil,
+ "instance checks.active.req_headers mutated in place")
+ ngx.say("passed")
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 2: auth.query is appended with & when http_path already has a query
string
+--- config
+ location /t {
+ content_by_lua_block {
+ local plugin = require("apisix.plugins.ai-proxy-multi")
+ local instance = {
+ name = "ins",
+ provider = "openai",
+ weight = 1,
+ auth = {
+ query = {
+ api_key = "secret",
+ },
+ },
+ options = {
+ model = "gpt-4",
+ },
+ override = {
+ endpoint = "http://127.0.0.1:16724",
+ },
+ checks = {
+ active = {
+ type = "http",
+ http_path = "/status?probe=ready",
+ healthy = {
+ interval = 1,
+ successes = 1,
+ },
+ unhealthy = {
+ interval = 1,
+ http_failures = 2,
+ },
+ },
+ },
+ }
+
+ for i = 1, 2 do
+ local upstream, err = plugin.construct_upstream(instance)
+ assert(upstream, err)
+ local http_path = upstream.checks.active.http_path
+ assert(http_path == "/status?probe=ready&api_key=secret",
+ "call " .. i .. ": unexpected http_path: " .. http_path)
+ end
+
+ assert(instance.checks.active.http_path == "/status?probe=ready",
+ "instance checks.active.http_path mutated in place: "
+ .. instance.checks.active.http_path)
+ ngx.say("passed")
+ }
+ }
+--- response_body
+passed