You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Requested by an APISIX committer while reviewing the AI-plugin cutover: apache/apisix#13778 (comment).
Problem
The client resolves hostnames through nginx's resolver directive and nothing else. A host application that owns its own resolver therefore cannot make this client agree with the rest of its outbound traffic.
In APISIX that gap is total. apisix/patch.lua wraps cosocket connect so every hostname goes through core.resolver:
core.resolver is a Lua DNS client (resty.dns.client) that reads /etc/hosts, honours the dns_resolver config, the resolv.conf search domains and SRV records, and keeps its own cache. Every cosocket-based client inherits that. This client dials from C, never touches a cosocket, and so sees only the resolver directive.
Concretely, with APISIX's test resolver (resolver 8.8.8.8 114.114.114.114) every localhost upstream fails:
connect: localhost could not be resolved (3: Host not found)
while lua-resty-http on the same route succeeds, because the name never reaches nginx's resolver.
Why a hook rather than reading the config
This is not the same shape as the trust-store fix in #43. There, the effective value lived in ngx_lua's loc conf and could be read from C. Here the resolver is a Lua module whose lookup yields, so C cannot call it synchronously. The resolution has to happen on the Lua side, before entering the FFI boundary — which also makes this cheap: a binding change, no C change.
Proposed shape
A resolver the host application installs once:
localclient=require("resty.ngx_http_ffi_client")
client.set_resolver(function (host)
returnip, err-- called only for non-IP hostsend)
APISIX would wire it once at init (core.resolver.parse_domain) and every plugin moving off lua-resty-http would then get parity for free. A per-call opts.resolver would also work, but the module-level form is what removes the per-plugin burden the reviewer is asking about.
Four details that matter, learned from implementing this as a workaround:
Resolve before the request struct is populated and before the keepalive pool key is derived. The pool key is built from opts.host, so if two names resolve to different addresses they must not share a pooled connection — the same invariant fix: fall back to lua_ssl_trusted_certificate for the trust store #43 established for the CA.
The Host header and the SNI must keep the original name. The binding currently defaults ssl_server_name = opts.ssl_server_name or opts.host, so substituting the host first would silently make the SNI an IP address and break certificate verification against any vhost-routed upstream. The name has to be captured before substitution.
IP literals should short-circuit, so the common case costs nothing.
A resolution failure should surface as a connect error, matching what a cosocket does today.
Current workaround, and what the hook would remove
APISIX pre-resolves in apisix/utils/http.lua, which the AI transport calls before connect():
function_M.resolve_upstream_host(params)
-- IP literals short-circuitlocalip, err=core.resolver.parse_domain(host)
...params.ssl_server_name=params.ssl_server_nameorhost-- keep the name in the Host header, with the port when it is not the scheme defaultparams.host=ipend
Moving it into a shared module answered the reviewer's immediate "duplicated across many plugins" concern, but every caller still has to remember to call it before connecting, which is a footgun rather than a fix. With the hook, that helper and its call sites can be deleted outright.
Requested by an APISIX committer while reviewing the AI-plugin cutover: apache/apisix#13778 (comment).
Problem
The client resolves hostnames through nginx's
resolverdirective and nothing else. A host application that owns its own resolver therefore cannot make this client agree with the rest of its outbound traffic.In APISIX that gap is total.
apisix/patch.luawraps cosocketconnectso every hostname goes throughcore.resolver:core.resolveris a Lua DNS client (resty.dns.client) that reads/etc/hosts, honours thedns_resolverconfig, the resolv.conf search domains and SRV records, and keeps its own cache. Every cosocket-based client inherits that. This client dials from C, never touches a cosocket, and so sees only theresolverdirective.Concretely, with APISIX's test resolver (
resolver 8.8.8.8 114.114.114.114) everylocalhostupstream fails:while
lua-resty-httpon the same route succeeds, because the name never reaches nginx's resolver.Why a hook rather than reading the config
This is not the same shape as the trust-store fix in #43. There, the effective value lived in ngx_lua's loc conf and could be read from C. Here the resolver is a Lua module whose lookup yields, so C cannot call it synchronously. The resolution has to happen on the Lua side, before entering the FFI boundary — which also makes this cheap: a binding change, no C change.
Proposed shape
A resolver the host application installs once:
APISIX would wire it once at init (
core.resolver.parse_domain) and every plugin moving off lua-resty-http would then get parity for free. A per-callopts.resolverwould also work, but the module-level form is what removes the per-plugin burden the reviewer is asking about.Four details that matter, learned from implementing this as a workaround:
opts.host, so if two names resolve to different addresses they must not share a pooled connection — the same invariant fix: fall back to lua_ssl_trusted_certificate for the trust store #43 established for the CA.Hostheader and the SNI must keep the original name. The binding currently defaultsssl_server_name = opts.ssl_server_name or opts.host, so substituting the host first would silently make the SNI an IP address and break certificate verification against any vhost-routed upstream. The name has to be captured before substitution.Current workaround, and what the hook would remove
APISIX pre-resolves in
apisix/utils/http.lua, which the AI transport calls beforeconnect():Moving it into a shared module answered the reviewer's immediate "duplicated across many plugins" concern, but every caller still has to remember to call it before connecting, which is a footgun rather than a fix. With the hook, that helper and its call sites can be deleted outright.
References