From 4e0d68c7fe2213203d94c3907509541aeeb3fd3c Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Mon, 20 Jul 2026 14:03:26 +0800 Subject: [PATCH 1/3] fix(batch-requests): fall back to node_listen port when served over a unix socket The plugin loops pipelined requests back to APISIX with `httpc:connect("127.0.0.1", ngx.var.server_port)`. When the inbound request arrives on a unix domain socket, nginx renders `$server_port` as an empty string, so the connect fails with "missing the port number" and the endpoint returns a 500. Fall back to the first TCP port in `apisix.node_listen` when `$server_port` is empty. The value read from the local conf is not normalized by the CLI, so both a plain port number and an array of port numbers or `{ip, port}` tables are handled. If no TCP port can be resolved, return a 503 explaining that batch-requests needs a TCP listener to loop back requests. Fixes #11781 --- apisix/plugins/batch-requests.lua | 44 +++++++++++++++++- t/plugin/batch-requests.t | 75 +++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/apisix/plugins/batch-requests.lua b/apisix/plugins/batch-requests.lua index 8927b33b5a50..6b09ffa04df6 100644 --- a/apisix/plugins/batch-requests.lua +++ b/apisix/plugins/batch-requests.lua @@ -20,6 +20,7 @@ local plugin = require("apisix.plugin") local ngx = ngx local ipairs = ipairs local pairs = pairs +local type = type local str_find = core.string.find local str_lower = string.lower @@ -224,6 +225,39 @@ local function set_common_query(data) end +-- When the request is received on a unix domain socket, $server_port is an +-- empty string, so we fall back to the first TCP port in `apisix.node_listen`. +-- The value read from the local conf is not normalized by the CLI, so it can +-- be a plain port number, or an array of port numbers or `{ip, port}` tables. +local function get_loopback_port() + local server_port = ngx.var.server_port + if server_port and server_port ~= "" then + return server_port + end + + local local_conf = core.config.local_conf() + local node_listen = core.table.try_read_attr(local_conf, "apisix", "node_listen") + if type(node_listen) == "number" then + return node_listen + end + + if type(node_listen) == "table" then + for _, value in ipairs(node_listen) do + if type(value) == "number" then + return value + end + + if type(value) == "table" then + -- the port defaults to 9080 if not set, see `apisix/cli/ops.lua` + return value.port or 9080 + end + end + end + + return nil +end + + local function batch_requests(ctx) local metadata = plugin.plugin_metadata(plugin_name) core.log.info("metadata: ", core.json.delay_encode(metadata)) @@ -269,9 +303,17 @@ local function batch_requests(ctx) } end + local server_port = get_loopback_port() + if not server_port then + return 503, { + error_msg = "this APISIX instance doesn't listen on a TCP port, " .. + "but the batch-requests plugin needs one to loop back requests" + } + end + local httpc = http.new() httpc:set_timeout(data.timeout) - local ok, err = httpc:connect("127.0.0.1", ngx.var.server_port) + local ok, err = httpc:connect("127.0.0.1", server_port) if not ok then return 500, {error_msg = "connect to apisix failed: " .. err} end diff --git a/t/plugin/batch-requests.t b/t/plugin/batch-requests.t index 298f9df11e92..6609cd12b91f 100644 --- a/t/plugin/batch-requests.t +++ b/t/plugin/batch-requests.t @@ -1194,3 +1194,78 @@ qr/property \\"path\\" is required/ GET /t --- response_body passed + + + +=== TEST 31: aggregate requests arriving via a unix domain socket +--- config + listen unix:$TEST_NGINX_HTML_DIR/apisix.sock; + + location = /t { + content_by_lua_block { + local core = require("apisix.core") + local json = require("toolkit.json") + local http = require("resty.http") + local httpc = http.new() + local ok, err = httpc:connect("unix:$TEST_NGINX_HTML_DIR/apisix.sock") + if not ok then + ngx.say("failed to connect: ", err) + return + end + + local body = core.json.encode({ + pipeline = { + {path = "/uds-b"}, + {path = "/uds-c", method = "PUT"}, + } + }) + + local res, err = httpc:request({ + method = "POST", + path = "/apisix/batch-requests", + headers = { + ["Host"] = "127.0.0.1", + ["Content-Type"] = "application/json", + }, + body = body, + }) + if not res then + ngx.say("request failed: ", err) + return + end + + local resp_body, err = res:read_body() + if not resp_body then + ngx.say("failed to read response body: ", err) + return + end + + if res.status ~= 200 then + ngx.status = res.status + ngx.print(resp_body) + return + end + + local data = json.decode(resp_body) + for _, resp in ipairs(data) do + ngx.say(resp.status, " ", resp.body) + end + } + } + + location = /uds-b { + content_by_lua_block { + ngx.print("B") + } + } + location = /uds-c { + content_by_lua_block { + ngx.status = 201 + ngx.print("C") + } + } +--- request +GET /t +--- response_body +200 B +201 C From 5d7175a43aa6f911397f3d6be890a592f60f5b47 Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Mon, 20 Jul 2026 14:11:05 +0800 Subject: [PATCH 2/3] fix(batch-requests): skip node_listen entries bound to a non-loopback address The loopback connect targets 127.0.0.1, so a node_listen entry bound to a specific external address does not necessarily accept it. Prefer the first entry that is reachable over loopback (no ip, 0.0.0.0 or 127.0.0.1) and only fall back to a specifically-bound entry when there is no other option. --- apisix/plugins/batch-requests.lua | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/apisix/plugins/batch-requests.lua b/apisix/plugins/batch-requests.lua index 6b09ffa04df6..b6f954978b34 100644 --- a/apisix/plugins/batch-requests.lua +++ b/apisix/plugins/batch-requests.lua @@ -226,9 +226,11 @@ end -- When the request is received on a unix domain socket, $server_port is an --- empty string, so we fall back to the first TCP port in `apisix.node_listen`. +-- empty string, so we fall back to a TCP port from `apisix.node_listen`. -- The value read from the local conf is not normalized by the CLI, so it can -- be a plain port number, or an array of port numbers or `{ip, port}` tables. +-- An entry bound to a specific address is only used as a last resort, as the +-- loopback connect below targets 127.0.0.1 and would otherwise be refused. local function get_loopback_port() local server_port = ngx.var.server_port if server_port and server_port ~= "" then @@ -242,6 +244,7 @@ local function get_loopback_port() end if type(node_listen) == "table" then + local bound_port for _, value in ipairs(node_listen) do if type(value) == "number" then return value @@ -249,9 +252,17 @@ local function get_loopback_port() if type(value) == "table" then -- the port defaults to 9080 if not set, see `apisix/cli/ops.lua` - return value.port or 9080 + local port = value.port or 9080 + local ip = value.ip + if ip == nil or ip == "0.0.0.0" or ip == "127.0.0.1" then + return port + end + + bound_port = bound_port or port end end + + return bound_port end return nil From be50eedc853a111e3224901f6221bfa4f99fb38a Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Mon, 20 Jul 2026 14:53:25 +0800 Subject: [PATCH 3/3] test(batch-requests): cover the no-TCP-port path and bound the client TEST 32 drives the 503 branch by leaving node_listen empty, which is the shape a unix-socket-only deployment has. Also set an explicit timeout on the test client so a stalled socket cannot hold the suite open. --- t/plugin/batch-requests.t | 55 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/t/plugin/batch-requests.t b/t/plugin/batch-requests.t index 6609cd12b91f..1fe8d66b03b2 100644 --- a/t/plugin/batch-requests.t +++ b/t/plugin/batch-requests.t @@ -1207,6 +1207,7 @@ passed local json = require("toolkit.json") local http = require("resty.http") local httpc = http.new() + httpc:set_timeout(2000) local ok, err = httpc:connect("unix:$TEST_NGINX_HTML_DIR/apisix.sock") if not ok then ngx.say("failed to connect: ", err) @@ -1269,3 +1270,57 @@ GET /t --- response_body 200 B 201 C + + + +=== TEST 32: no TCP port to loop back through +--- yaml_config +apisix: + node_listen: [] + enable_resolv_search_opt: false + trusted_addresses: + - "127.0.0.1" +--- config + listen unix:$TEST_NGINX_HTML_DIR/apisix.sock; + + location = /t { + content_by_lua_block { + local core = require("apisix.core") + local http = require("resty.http") + local httpc = http.new() + httpc:set_timeout(2000) + local ok, err = httpc:connect("unix:$TEST_NGINX_HTML_DIR/apisix.sock") + if not ok then + ngx.say("failed to connect: ", err) + return + end + + local body = core.json.encode({pipeline = {{path = "/uds-b"}}}) + local res, err = httpc:request({ + method = "POST", + path = "/apisix/batch-requests", + headers = { + ["Host"] = "127.0.0.1", + ["Content-Type"] = "application/json", + }, + body = body, + }) + if not res then + ngx.say("request failed: ", err) + return + end + + local resp_body = res:read_body() + ngx.say(res.status, " ", resp_body) + } + } + + location = /uds-b { + content_by_lua_block { + ngx.print("B") + } + } +--- request +GET /t +--- response_body_like +503 .*doesn't listen on a TCP port.*