Skip to content

Commit a21da28

Browse files
refactor(ai-transport): move client selection into apisix.utils.http
The selection, validation and DNS-parity logic sat inside the AI transport, so forward-auth, http-logger and anything else moving off lua-resty-http could not reuse it. It now lives in apisix/utils/http.lua alongside the other shared client factories: the module owns the client names, the schema fragment, loading and caching, and resolve_upstream_host(); the caller owns where the preference comes from and passes the name in. No behaviour change: the transport still reads plugin_attr.ai-proxy.http_client and the whole suite is unchanged.
1 parent 1262f54 commit a21da28

3 files changed

Lines changed: 175 additions & 96 deletions

File tree

apisix/plugins/ai-transport/http.lua

Lines changed: 20 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -19,52 +19,34 @@
1919
-- Provides HTTP client lifecycle management for AI provider requests.
2020

2121
local core = require("apisix.core")
22+
local http_client = require("apisix.utils.http")
2223
local ngx_now = ngx.now
2324
local pairs = pairs
2425
local ipairs = ipairs
2526
local pcall = pcall
26-
local require = require
2727
local type = type
2828
local str_lower = string.lower
29-
local tonumber = tonumber
3029
local tostring = tostring
3130

32-
local FFI_CLIENT = "ngx_http_ffi_client"
33-
local LUA_RESTY_HTTP = "lua-resty-http"
34-
35-
-- the client name in the config is not the module name
36-
local CLIENT_MODULES = {
37-
[FFI_CLIENT] = "resty.ngx_http_ffi_client",
38-
[LUA_RESTY_HTTP] = "resty.http",
39-
}
40-
4131
local attr_schema = {
4232
type = "object",
4333
properties = {
44-
http_client = {
45-
type = "string",
46-
enum = {FFI_CLIENT, LUA_RESTY_HTTP},
47-
default = FFI_CLIENT,
48-
},
34+
http_client = http_client.client_schema,
4935
},
5036
}
5137

5238
local _M = {}
5339

54-
local http_client
55-
local http_client_is_ffi
40+
local client_name
5641

5742

58-
--- Pick the outbound HTTP client.
59-
-- `plugin_attr.ai-proxy.http_client` names it: "ngx_http_ffi_client", the
60-
-- default, or "lua-resty-http". The first is a C client with the same object
61-
-- API as the second and around half its outbound CPU cost, and it exists only
62-
-- when the gateway runtime was built with the module.
63-
-- Resolved on first request, because local_conf is not readable while the
64-
-- module is still loading, and cached only once a client has been loaded.
65-
local function resolve_client()
66-
if http_client then
67-
return http_client
43+
--- Which client this transport should use.
44+
-- `plugin_attr.ai-proxy.http_client` names it; the shared module owns the
45+
-- names, validation and loading. Read on first request, because local_conf is
46+
-- not readable while this module is still loading.
47+
local function resolve_client_name()
48+
if client_name then
49+
return client_name
6850
end
6951

7052
local local_conf = core.config.local_conf()
@@ -76,72 +58,9 @@ local function resolve_client()
7658
return nil, "invalid plugin_attr.ai-proxy: " .. err
7759
end
7860

79-
local name = attr.http_client or FFI_CLIENT
80-
local module_name = CLIENT_MODULES[name]
61+
client_name = attr.http_client or http_client.DEFAULT_CLIENT
8162

82-
local mod
83-
ok, mod = pcall(require, module_name)
84-
if not ok or type(mod) ~= "table" then
85-
core.log.error(module_name, " is not available: ", mod)
86-
return nil, module_name .. " is not available: " .. tostring(mod)
87-
end
88-
89-
http_client = mod
90-
http_client_is_ffi = name == FFI_CLIENT
91-
92-
return http_client
93-
end
94-
95-
96-
--- Resolve the upstream name the way every other socket in the gateway does.
97-
-- Cosockets are patched (apisix/patch.lua) to run names through
98-
-- core.resolver, which honours dns_resolver, /etc/hosts and the search
99-
-- domains. The C client dials on its own and only sees nginx's `resolver`,
100-
-- so the name is resolved here and kept for the Host header and the SNI.
101-
local function resolve_upstream_host(params)
102-
local host = params.host
103-
if not host
104-
or core.utils.parse_ipv4(host)
105-
or core.utils.parse_ipv6(host)
106-
then
107-
return true
108-
end
109-
110-
local ip, err = core.resolver.parse_domain(host)
111-
if not ip then
112-
return nil, "failed to parse domain: " .. (err or "unknown")
113-
end
114-
115-
params.ssl_server_name = params.ssl_server_name or host
116-
117-
local headers = params.headers or {}
118-
if not headers["Host"] and not headers["host"] then
119-
local default_port = params.scheme == "https" and 443 or 80
120-
if params.port and tonumber(params.port) ~= default_port then
121-
headers["Host"] = host .. ":" .. params.port
122-
else
123-
headers["Host"] = host
124-
end
125-
end
126-
params.headers = headers
127-
128-
params.host = ip
129-
130-
return true
131-
end
132-
133-
134-
--- Create an HTTP client.
135-
-- The Lua half of `ngx_http_ffi_client` loads even when the C module is not
136-
-- compiled into the runtime; new() is what reports that. Either way the
137-
-- failure is returned, never worked around with the other client.
138-
local function new_client()
139-
local client, err = resolve_client()
140-
if not client then
141-
return nil, err
142-
end
143-
144-
return client.new()
63+
return client_name
14564
end
14665

14766

@@ -214,7 +133,12 @@ end
214133
-- @return string|nil Error message
215134
-- @return table|nil Upstream metadata on failure (for recording failed attempts)
216135
function _M.request(params, timeout)
217-
local httpc, err = new_client()
136+
local name, name_err = resolve_client_name()
137+
if not name then
138+
return nil, "failed to create http client: " .. name_err
139+
end
140+
141+
local httpc, err = http_client.new(name)
218142
if not httpc then
219143
return nil, "failed to create http client: " .. (err or "unknown")
220144
end
@@ -225,8 +149,8 @@ function _M.request(params, timeout)
225149
local upstream_scheme = params.scheme or "http"
226150
local t0 = ngx_now()
227151

228-
if http_client_is_ffi then
229-
local resolved, rerr = resolve_upstream_host(params)
152+
if http_client.needs_resolve(name) then
153+
local resolved, rerr = http_client.resolve_upstream_host(params)
230154
if not resolved then
231155
return nil, "connect: " .. rerr, {
232156
upstream_addr = upstream_addr,

apisix/utils/http.lua

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
--
2+
-- Licensed to the Apache Software Foundation (ASF) under one or more
3+
-- contributor license agreements. See the NOTICE file distributed with
4+
-- this work for additional information regarding copyright ownership.
5+
-- The ASF licenses this file to You under the Apache License, Version 2.0
6+
-- (the "License"); you may not use this file except in compliance with
7+
-- the License. You may obtain a copy of the License at
8+
--
9+
-- http://www.apache.org/licenses/LICENSE-2.0
10+
--
11+
-- Unless required by applicable law or agreed to in writing, software
12+
-- distributed under the License is distributed on an "AS IS" BASIS,
13+
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
-- See the License for the specific language governing permissions and
15+
-- limitations under the License.
16+
--
17+
18+
--- Outbound HTTP client selection.
19+
-- Shared by any module that makes outbound HTTP calls, so the choice between
20+
-- `ngx_http_ffi_client` and `lua-resty-http` is made in one place rather than
21+
-- per plugin. The caller decides where the preference comes from (its own
22+
-- config key) and passes the name in.
23+
24+
local core = require("apisix.core")
25+
local pcall = pcall
26+
local require = require
27+
local tonumber = tonumber
28+
local tostring = tostring
29+
local type = type
30+
31+
local FFI_CLIENT = "ngx_http_ffi_client"
32+
local LUA_RESTY_HTTP = "lua-resty-http"
33+
34+
-- the client name used in configuration is not the module name
35+
local CLIENT_MODULES = {
36+
[FFI_CLIENT] = "resty.ngx_http_ffi_client",
37+
[LUA_RESTY_HTTP] = "resty.http",
38+
}
39+
40+
local loaded = {}
41+
42+
43+
local _M = {
44+
version = 0.1,
45+
FFI_CLIENT = FFI_CLIENT,
46+
LUA_RESTY_HTTP = LUA_RESTY_HTTP,
47+
DEFAULT_CLIENT = FFI_CLIENT,
48+
}
49+
50+
51+
--- Schema fragment for a client-name config field.
52+
-- Callers embed this in their own attribute schema so every module validates
53+
-- the name the same way.
54+
_M.client_schema = {
55+
type = "string",
56+
enum = {FFI_CLIENT, LUA_RESTY_HTTP},
57+
default = FFI_CLIENT,
58+
}
59+
60+
61+
--- Load the module for a client name.
62+
-- `ngx_http_ffi_client` is a C client with the same object API as
63+
-- lua-resty-http and around half its outbound CPU cost, and it exists only
64+
-- when the gateway runtime was built with the module. A name that cannot be
65+
-- loaded is an error, never a silent switch to the other client.
66+
-- Cached per name once loaded, so a failure is retried rather than remembered.
67+
local function load_client(name)
68+
local cached = loaded[name]
69+
if cached then
70+
return cached
71+
end
72+
73+
local module_name = CLIENT_MODULES[name]
74+
if not module_name then
75+
return nil, "unknown http client: " .. tostring(name)
76+
end
77+
78+
local ok, mod = pcall(require, module_name)
79+
if not ok or type(mod) ~= "table" then
80+
core.log.error(module_name, " is not available: ", mod)
81+
return nil, module_name .. " is not available: " .. tostring(mod)
82+
end
83+
84+
loaded[name] = mod
85+
86+
return mod
87+
end
88+
89+
90+
--- Create an HTTP client.
91+
-- @tparam string|nil name client name; defaults to DEFAULT_CLIENT
92+
-- @treturn table|nil the client
93+
-- @treturn string|nil error message
94+
function _M.new(name)
95+
name = name or _M.DEFAULT_CLIENT
96+
97+
local mod, err = load_client(name)
98+
if not mod then
99+
return nil, err
100+
end
101+
102+
-- The Lua half of `ngx_http_ffi_client` loads even when the C module is
103+
-- not compiled into the runtime; new() is what reports that.
104+
return mod.new()
105+
end
106+
107+
108+
--- Whether a client name needs resolve_upstream_host() before connecting.
109+
function _M.needs_resolve(name)
110+
return (name or _M.DEFAULT_CLIENT) == FFI_CLIENT
111+
end
112+
113+
114+
--- Resolve the upstream name the way every other socket in the gateway does.
115+
-- Cosockets are patched (apisix/patch.lua) to run names through core.resolver,
116+
-- which honours dns_resolver, /etc/hosts and the search domains. A client that
117+
-- dials from C never touches a cosocket and only sees nginx's `resolver`, so
118+
-- the name is resolved here and kept for the Host header and the SNI.
119+
-- Mutates `params` in place.
120+
-- @treturn boolean|nil true on success
121+
-- @treturn string|nil error message
122+
function _M.resolve_upstream_host(params)
123+
local host = params.host
124+
if not host
125+
or core.utils.parse_ipv4(host)
126+
or core.utils.parse_ipv6(host)
127+
then
128+
return true
129+
end
130+
131+
local ip, err = core.resolver.parse_domain(host)
132+
if not ip then
133+
return nil, "failed to parse domain: " .. (err or "unknown")
134+
end
135+
136+
params.ssl_server_name = params.ssl_server_name or host
137+
138+
local headers = params.headers or {}
139+
if not headers["Host"] and not headers["host"] then
140+
local default_port = params.scheme == "https" and 443 or 80
141+
if params.port and tonumber(params.port) ~= default_port then
142+
headers["Host"] = host .. ":" .. params.port
143+
else
144+
headers["Host"] = host
145+
end
146+
end
147+
params.headers = headers
148+
149+
params.host = ip
150+
151+
return true
152+
end
153+
154+
155+
return _M
7.99 MB
Binary file not shown.

0 commit comments

Comments
 (0)