From f4e3899526c09e68450102034fe3b551cd05f740 Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Fri, 12 Jun 2026 13:05:03 +0800 Subject: [PATCH] fix(ai-proxy): forward client method and query string for passthrough MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The passthrough protocol is a catch-all that proxies the original client request to the provider, but the upstream request builder hardcoded the method to POST and built the query string only from auth.query and the override.endpoint URL — the client's own query string (ctx.var.args) was dropped. This breaks providers that carry required parameters in the query on POST requests, e.g. Azure OpenAI's ?api-version=. For the passthrough protocol, forward the client's HTTP method and merge its query string into the upstream request. Signed-off-by: AlinsRan --- apisix/plugins/ai-providers/base.lua | 14 ++- t/plugin/ai-proxy-passthrough.t | 153 +++++++++++++++++++++++++++ 2 files changed, 166 insertions(+), 1 deletion(-) diff --git a/apisix/plugins/ai-providers/base.lua b/apisix/plugins/ai-providers/base.lua index 3021b89cdd4b..33403329b853 100644 --- a/apisix/plugins/ai-providers/base.lua +++ b/apisix/plugins/ai-providers/base.lua @@ -201,8 +201,20 @@ function _M.build_request(self, ctx, conf, request_body, opts) ctx.ai_converter.convert_headers(headers) end + -- For the passthrough protocol the gateway acts as a catch-all proxy, so + -- forward the client's HTTP method and original query string unchanged. + -- Other protocols always issue a POST with provider-specific query args. + local method = "POST" + if ctx.ai_target_protocol == "passthrough" then + method = core.request.get_method() + local client_args = ctx.var.args and core.string.decode_args(ctx.var.args) + if type(client_args) == "table" then + core.table.merge(query_params, client_args) + end + end + local params = { - method = "POST", + method = method, scheme = scheme, headers = headers, ssl_verify = conf.ssl_verify, diff --git a/t/plugin/ai-proxy-passthrough.t b/t/plugin/ai-proxy-passthrough.t index b01e7a513c90..d4b7ad6889ac 100644 --- a/t/plugin/ai-proxy-passthrough.t +++ b/t/plugin/ai-proxy-passthrough.t @@ -278,3 +278,156 @@ images: passthrough empty: nil --- no_error_log no matching AI protocol + + + +=== TEST 10: set route for passthrough query/method forwarding +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/plugin_proxy_rewrite_args", + "plugins": { + "ai-proxy": { + "provider": "openai", + "auth": { + "header": { + "Authorization": "Bearer token" + } + }, + "override": { + "endpoint": "http://127.0.0.1:1980" + }, + "ssl_verify": false + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 11: passthrough forwards the client query string to the upstream +--- request +POST /plugin_proxy_rewrite_args?name=foo +{"prompt":"x"} +--- more_headers +Content-Type: application/json +--- response_body eval +qr/name: foo/ +--- no_error_log +no matching AI protocol + + + +=== TEST 12: set route for passthrough method forwarding +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/plugin_proxy_rewrite", + "plugins": { + "ai-proxy": { + "provider": "openai", + "auth": { + "header": { + "Authorization": "Bearer token" + } + }, + "override": { + "endpoint": "http://127.0.0.1:1980" + }, + "ssl_verify": false + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 13: passthrough forwards the client HTTP method to the upstream +--- request +PUT /plugin_proxy_rewrite +{"prompt":"x"} +--- more_headers +Content-Type: application/json +--- error_log +plugin_proxy_rewrite get method: PUT +--- no_error_log +no matching AI protocol + + + +=== TEST 14: set route whose override.endpoint carries its own query args +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/plugin_proxy_rewrite_args", + "plugins": { + "ai-proxy": { + "provider": "openai", + "auth": { + "header": { + "Authorization": "Bearer token" + } + }, + "override": { + "endpoint": "http://127.0.0.1:1980/plugin_proxy_rewrite_args?name=fromendpoint&ekey=eval" + }, + "ssl_verify": false + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 15: client query overrides the endpoint query on conflicting keys +--- request +POST /plugin_proxy_rewrite_args?name=fromclient&ckey=cval +{"prompt":"x"} +--- more_headers +Content-Type: application/json +--- response_body +uri: /plugin_proxy_rewrite_args +ckey: cval +ekey: eval +name: fromclient +--- no_error_log +no matching AI protocol