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

Reply via email to