APISIX init.lua Source Code Analysis
Categories: APISIX
APISIX apisix/init.lua Source Code Analysis
Target:
apisix/init.lua(line numbers are based on the current master branch of the repository) This file is the request lifecycle entry point of the APISIX gateway, shared by both the HTTP and Stream subsystems.
1. File Positioning
init.lua does not handle concrete business logic itself; it takes on three kinds of responsibilities:
- Process lifecycle hooks: initialization and cleanup for the
init/init_worker/exit_workerphases; - Request lifecycle orchestration: invoking the routing, plugin, and load-balancing modules in sequence across Nginx phases (
access→balancer→header_filter→body_filter→log); - Security and protocol handling: mTLS verification, URI normalization, X-Forwarded-* anti-spoofing, gRPC/Dubbo internal redirects, etc.
The functions in this file are called from the phase directives of the nginx.conf rendered by apisix/cli/ngx_tpl.lua (the config template).
2. Wiring to Nginx Phases
| Nginx directive | init.lua function | Template location |
|---|---|---|
init_by_lua_block (http) |
http_init(args) |
ngx_tpl.lua:514 |
init_worker_by_lua_block (http) |
http_init_worker() |
:523 |
exit_worker_by_lua_block (http) |
http_exit_worker() |
:527 |
init_by_lua_block (stream) |
stream_init(args) |
:205 |
init_worker_by_lua_block (stream) |
stream_init_worker() |
:209 |
ssl_client_hello_by_lua_block |
ssl_client_hello_phase() |
:234 / :772 |
ssl_certificate_by_lua_block |
ssl_phase() |
:238 / :776 |
preread_by_lua_block (stream) |
stream_preread_phase() |
:247 |
log_by_lua_block (stream) |
stream_log_phase() |
:259 |
access_by_lua_block (http main server) |
http_access_phase() |
:830 |
balancer_by_lua_block (http upstream) |
http_balancer_phase() |
:465 / :469 / :482 |
header_filter_by_lua_block |
http_header_filter_phase() |
:878 / :914 / :937 |
body_filter_by_lua_block |
http_body_filter_phase() |
:882 / :918 / :941 |
log_by_lua_block (http) |
http_log_phase() |
:886 / :922 / :945 |
access_by_lua of location @grpc_pass |
grpc_access_phase() |
:893 |
access_by_lua of location @dubbo_pass |
dubbo_access_phase() |
:929 |
content_by_lua of the control server |
http_control() |
:549 |
content_by_lua of /apisix/admin |
http_admin() |
:645 |
/apisix/status |
status() |
:561 |
/apisix/status/ready |
status_ready() |
:566 |
3. Module-Load-Time Initialization (L17 ~ L88)
3.1 JIT Parameters (L17 ~ L26)
if require("ffi").os == "Linux" then
require("ngx.re").opt("jit_stack_size", 200 * 1024)
end
- The PCRE JIT stack size is only adjusted on Linux, and it must be placed before any regex compilation (otherwise it reports “changing jit stack size is not allowed…”).
jit.opt.start(...)tuning parameters:minstitch=2: stitch traces only when there are at least 2 traces;maxtrace=4000/maxrecord=8000: cap the number of traces and the record length of a single trace, preventing JIT memory from running away;sizemcode=64/maxmcode=4000: mcode area size (starts at 64KB, capped at 4000KB);maxirconst=1000: IR constant limit.
3.2 Module Dependencies (L28 ~ L69)
Core dependencies and their roles:
| Module | Role |
|---|---|
apisix.patch |
Runtime patches for OpenResty/ngx_lua |
apisix.core |
Infrastructure: logging, json, tablepool, lrucache, etc. |
apisix.plugin / plugin_config / consumer_group |
Plugin system, plugin configs, consumer groups |
apisix.router |
Route matching: router_http (HTTP), router_ssl (certificates), router_stream (Stream) |
apisix.upstream / apisix.balancer |
Upstream management, load balancing |
apisix.ssl |
TLS utilities (SNI, protocols, sessions) |
apisix.stream.xrpc |
Protocol dispatch within the Stream subsystem |
apisix.tracer |
Distributed tracing spans |
apisix.pubsub.kafka |
Special handling for kafka-type upstreams |
apisix.utils.trusted-addresses |
Trusted-address detection (for X-Forwarded-* anti-spoofing) |
3.3 Key Global Flags
local is_http = false
if ngx.config.subsystem == "http" then
is_http = true
control_api_router = require("apisix.control.router")
end
is_http: distinguishes the subsystem; within the same codebase, HTTP usescore.response.exit(...)while Stream usesngx_exit(...).apisix_base_flags(L77 ~ L80):resty.apisix.patchis APISIX’s customized C module (apisix-base); a pcall loading failure degrades to an empty table. A typical use is theclient_cert_verified_in_handshakeflag at L402 — when the newer apisix-base has already completed client certificate verification during the handshake, there is no need to check$ssl_client_verifyagain.apisix_ngx_client(L86): fromresty.apisix.client, providesenable_mirror()(used at L880 for gRPC traffic mirroring).ver_header(L84): the responseServerheader, degraded to"APISIX"whenenable_server_tokens=false(L162 ~ L164).
4. HTTP Subsystem
4.1 http_init (L91 ~ L110) — init Phase
- Initialize the DNS resolver, ID generator, and environment variables;
- Enable the privileged agent process (for privileged operations such as dynamically setting upstreams);
- Initialize the config center (
core.config.init, etcd / standalone yaml); xrpc.init().
4.2 http_init_worker (L113 ~ L171) — init_worker Phase
Initializes each subsystem in order, the order reflecting their dependencies:
- Random seed: preferentially taken from
/dev/urandom(L114); the “random test” log at L121 is actually a probe for test assertions (tests rely on it to confirm that init_worker has executed); - Event system, lrucache, service discovery, balancer, admin, background timers, debug;
- Worker-level initialization of the config center;
- Plugin system: plugin → router → service → plugin_config → consumer → consumer_group → secret → global_rules → upstream → ext-plugin;
- Control API router,
local_confcache,Serverheader version; - Prometheus plugin initialization (must come last so that metrics of all workers are ready);
- Trusted-address utility.
4.3 TLS / mTLS Handling
ssl_phase (L182 ~ L190)
ssl_certificate_by_lua phase: sets the certificate config matched during the ssl_client_hello_phase (ngx.ctx.matched_ssl) on the current TLS session.
ssl_client_hello_phase (L193 ~ L240)
- Must obtain the SNI (L195 ~ L200); exit directly if unavailable (accessing by IP, or a too-old protocol);
- Read the OCSP
status_requestextension; - Take
api_ctxfrom tablepool for matching (note L214 releases it immediately after use; it is not reused in this phase); router_ssl.match_and_set(api_ctx, true, sni)matches the SNI certificate;- Dynamically set
ssl_protocolsaccording to the match result (L228); - L238: store the SNI into
ngx.ctx.client_hello_sni— because in the Stream subsystem’s preread phase,ngx.ssl.server_name()returns the session hostname rather than the real SNI; recording it here allows later verification.
The Three mTLS Verification Functions
verify_tls_session_resumption(L318 ~ L331): TLS session resumption security check. When a session is resumed, verify that the hostname in the session matches the SNI of the current client hello, preventing mTLS from being bypassed via session resumption.verify_tls_client(L334 ~ L359, used by Stream): re-match the certificate by SNI; if aclientcertificate is configured and client verification is supported, check$ssl_client_verify == "SUCCESS"and perform the session-resumption check.verify_https_client(L371 ~ L434, used by HTTP):- Support
skip_mtls_uri_regex: exempt matching URIs from client certificates (L363 ~ L368, L381); - Certificate verification (preferentially trusting the handshake-time verification flag from apisix-base, L402);
- SNI vs Host consistency check (L417 ~ L426): prevent cross-domain certificate reuse where a user configures a
*.domainwildcard certificate but accesses with SNIa.domainwhile carrying Hostb.domain; - Session-resumption check.
- Support
4.4 http_access_phase (L691 ~ L857) — Core of the access Phase
Execution order:
- HTTP/3 adaptation (L694 ~ L696): when
ngx.req.http_version() == 3, convert the:authoritypseudo-header intoupstream_host; - api_ctx creation (L700 ~ L704): take from tablepool, attach to
ngx.ctx, and initialize thevarmetatable (lazily reads nginx variables); - mTLS verification (L708);
- Dynamic debugging (L712);
- URI normalization (L714 ~ L735):
delete_uri_tail_slash: remove the trailing/;normalize_uri_like_servlet: emulate Java Servlet semantics, truncate path parameters after;, and perform anti-bypass checks (./..segments, empty segments,%2eencoded dot segments);
- request_uri anti-injection rewrite (L740 ~ L741): only the normalized URI is written into
request_uri; the original value is stored inreal_request_uri; - X-Forwarded-* anti-spoofing (L743, see §5.4);
- Route matching (L745 ~ L759):
router.router_http.match(api_ctx); on a miss → still run global rules → return 404; - Config merging (L767 ~ L802):
plugin_config→service(merge_service_route), set context fields such asconf_type/conf_version/conf_id/route_id; - Run global rules (L807 ~ L808);
- Plugin/script execution (L810 ~ L851):
- Script routes: directly
script.run("access"); - Plugin routes:
plugin.filterfilters out the plugins active on this route → run therewritephase first → if the rewrite phase produced aconsumer(e.g., key-auth succeeded), merge the consumer / consumer_group config; if the config changed, re-filter and additionally run therewrite_in_consumerphase → finally run theaccessphase;
- Script routes: directly
handle_upstream(L854);- Set upstream X-Forwarded-* headers (L856).
4.5 handle_upstream (L493 ~ L610)
bypass_nginx_upstream: when a plugin (e.g., ai-proxy) requests the upstream with its own HTTP client, skip the nginx proxy and only run thebefore_proxyplugin phase (L495 ~ L498);- Determine the upstream:
upstream_id/ traffic-split overriddenupstream_id(L500 ~ L535); routes with domain nodes (has_domain) triggerparse_domain_in_route(L258 ~ L287) for DNS resolution, maintaining node versions via_nodes_verand theresourcemodule; - Upstream mTLS (L537 ~ L558):
upstream.tls.client_cert_idfetches the client certificate fromrouter_ssl; - WebSocket (L560 ~ L564): pass through the
Upgrade/Connectionheaders; - kafka upstream: handed over to
pubsub_kafka.access(L569 ~ L571); set_upstreamsets proxy parameters →load_balancer.pick_serverselects a node (L573 ~ L587);set_upstream_headers(L587): set the Host according topass_hostsemantics (pass/rewrite/node, L290 ~ L308);before_proxyruns early (L590): the before_proxy plugins run in the access phase; the comment explains that this is to “avoid always reinitializing the request”;_apisix_proxiedmarker (L592 ~ L598): must be set after before_proxy and before the proxy_pass dispatch; the log phase uses it to distinguish “nginx proxy errors” from “upstream responses”;- gRPC / Dubbo dispatch (L600 ~ L609):
stash_ngx_ctx()thenngx.exec("@grpc_pass")/"@dubbo_pass".
4.6 Passing Context Across Internal Redirects (L243 ~ L256)
ngx.exec internal redirects lose ngx.ctx; APISIX solves this with resty.ctxdump:
stash_ngx_ctx():ctxdump.stash_ngx_ctx()serializes the context; the reference number is written into the nginx variable$ctx_ref;fetch_ctx(): restoresngx.ctxin the target location by reference number, and clears the variable.
4.7 Response and Log Phases
http_header_filter_phase(L910 ~ L938):- Sets the
Serverheader; X-APISIX-Upstream-Status: shows the upstream status code per configuration, by default only on 5xx (L885 ~ L907);- Runs the
header_filterplugin phase; - Outputs the debug header
Apisix-Plugins(L927 ~ L934); - Pre-creates the span for the body_filter phase at the end.
- Sets the
http_body_filter_phase(L941 ~ L944): runs thebody_filteranddelayed_body_filterplugin phases.http_log_phase(L1088 ~ L1123):- End all tracer spans;
- Fallback for
apisix_upstream_response_time; - Run the
logplugin phase; - Passive health check reporting (L1104, §4.8);
after_balancehook;- Unified resource release: release_vars, release the
plugins/matched_route_record/api_ctxtablepool objects — everything borrowed from tablepool in the access phase is returned here.
4.8 Passive Health Check healthcheck_passive (L947 ~ L1006)
- Depends on
api_ctx.up_checker(created in the balancer phase); - HTTP: reports according to the configured healthy/unhealthy
http_statuses; - Stream: reports TCP failure for anything other than 200.
4.9 Admin / Control Interfaces
status(L1009): directly returns 200;status_ready(L1064): readiness probe, two checks:config_ready_check(L1031): the role’s config_provider must be yaml/etcd; thestatus-reportshdict must contain a ready record for every worker (key count == worker count, and values non-empty);discovery_ready_check(L1014): each service discovery module may optionally implementcheck_discovery_ready;
http_admin(L1169): Admin API route dispatch + CORS (OPTIONS preflight returns 200 directly) + JSON Content-Type; the router instance is cached in a do-block upvalue and initialized only once;http_control(L1191): control API route matching.
4.10 Load Balancing http_balancer_phase (L1126 ~ L1134)
balancer_by_lua phase: verifies that api_ctx exists, then calls load_balancer.run; the real node selection (peer setting) happens here — the pick_server in the access phase is only a pre-selection.
5. X-Forwarded-* Anti-Spoofing (L613 ~ L688)
Two steps, working with the template’s proxy_set_header X-Forwarded-XXX $var_x_forwarded_xxx; pattern:
handle_x_forwarded_headers(access phase, for untrusted sources):- Back up the original values to
original_x_forwarded_*; - Overwrite them with the values observed by APISIX itself (scheme, host, port);
- Clear
X-Forwarded-For(regenerated by$proxy_add_x_forwarded_for) and the RFC 7239Forwardedheader; - Sync the
var.http_x_forwarded_*cache.
- Back up the original values to
set_upstream_x_forwarded_headers(end of access): write the trusted values back into thevar_x_forwarded_*variables so that the proxy_set_header in the template takes effect.
Trusted sources are determined by apisix.utils.trusted-addresses (based on realip_remote_addr).
6. gRPC and Dubbo Internal Redirects (L860 ~ L882)
dubbo_access_phase: only restores the context;grpc_access_phase: restores the context →set_grpcs_upstream_param→ if traffic mirroring is enabled and apisix-ngx-client is loaded, callsenable_mirror().
Both locations share the main server’s header/body/log phase handlers (see the table in §2).
7. Stream Subsystem
7.1 Initialization (L1199 ~ L1254)
Similar to HTTP but leaner: resolver → config center → xrpc → worker-level initialization (lrucache, config center, plugin, xrpc, router.stream, service, upstream, events, admin, discovery, balancer).
7.2 stream_preread_phase (L1257 ~ L1371)
- mTLS verification (L1262,
verify_tls_client); - Route matching
router_stream.match; - Upstream determination: three sources —
upstream_id/service_id(merge_service_stream_route) / route inline upstream (includinghas_domainDNS resolution); plugin.stream_filterfilters plugins → run theprereadplugin phase;- Protocol dispatch (L1350 ~ L1353): when
protocolis configured, hand over toxrpc.run_protocol(e.g., Redis, Kafka protocol plugins); otherwise, direct TCP passthrough; - TCP direct path: set_upstream → pick_server →
before_proxy.
7.3 Subsequent Phases
stream_balancer_phase(L1374): same as HTTP, callsload_balancer.run;stream_log_phase(L1386): log plugins → passive health check → release api_ctx (note that, unlike HTTP, it directly doesplugin.run_plugin("log")and retrieves api_ctx).
8. HTTP Request Lifecycle Overview
client ──▶ nginx
│ ssl_client_hello_phase: match certificate by SNI, record client_hello_sni
│ ssl_phase: set certificate on the session
│ access: http_access_phase
│ ├─ verify_https_client (mTLS)
│ ├─ URI normalization / request_uri rewrite
│ ├─ X-Forwarded-* anti-spoofing
│ ├─ router_http.match
│ ├─ plugin_config / service / consumer merge
│ ├─ global rules → rewrite → (consumer) → access plugins
│ ├─ handle_upstream:
│ │ ├─ select upstream, DNS resolution, upstream mTLS certificate
│ │ ├─ pick_server (pre-selection)
│ │ ├─ before_proxy plugins
│ │ └─ grpc/dubbo? ──▶ stash ctx ──▶ ngx.exec(@grpc_pass / @dubbo_pass)
│ └─ set_upstream_x_forwarded_headers
│ balancer: http_balancer_phase ──▶ load_balancer.run (real peer selection)
│ @grpc_pass / @dubbo_pass: fetch ctx ──▶ restore context
│ header_filter / body_filter: http_header/body_filter_phase
│ log: http_log_phase
│ └─ log plugins → passive health check → release tablepool resources
└─▶ upstream
9. Security Design Highlights
| Mechanism | Location | Protects against |
|---|---|---|
| TLS session resumption SNI check | L318 ~ L331 | bypassing mTLS via session reuse |
| SNI vs Host consistency check | L417 ~ L426 | wildcard certificate cross-domain reuse |
skip_mtls_uri_regex exact matching |
L362 ~ L368 | bypass of certificate-exempt URIs |
URI ; parameter normalization + dot-segment/encoding checks |
L437 ~ L472 | path parameter injection, directory traversal |
request_uri rewrite |
L740 ~ L741 | injection via unnormalized request_uri |
| X-Forwarded-* rewrite and Forwarded clearing | L613 ~ L656 | spoofed client addresses |
| Log redaction | L281 ~ L283 | removes plugins/auth_conf before printing routes |
10. Performance and Resource Management Highlights
- tablepool:
api_ctxis fetched from the pool in the access phase and returned in the log phase (L1122 / L1401); same forpluginsandmatched_route_record. Exiting abnormally without returning them risks leaks, so every phase handler performs existence checks. - Lazy variables:
core.ctx.set_vars_meta(api_ctx)builds thevarmetatable; nginx variables are read on demand. - Localization: L53 ~ L68 localizes hot functions (ngx.now, ipairs, etc.) to reduce global lookups.
- JIT parameters: see §3.1, controlling trace count and memory caps.
- DNS resolution cache:
parse_domain_in_routecaches resolution results viaresource.set_nodes_ver_and_nodes, avoiding a DNS query on every request. - Passive health check: only reported in the log phase, off the forwarding path.
11. Notes and Pitfalls
- Stream and HTTP differ in exit-code semantics: the same function branches on
is_http(e.g., L510 ~ L514) — HTTP returns 502 with a JSON body; Stream can onlyngx_exit(1). local_confgets refreshed repeatedly: cached once inhttp_init_worker(L160), butset_resp_upstream_status,cors_admin, andstatus_readycallcore.config.local_conf()again — deliberate (config may be hot-updated in standalone mode).- The api_ctx in
ssl_client_hello_phaseis returned right after use (L214 ~ L215): it is not the same api_ctx as in the access phase and cannot be reused. - before_proxy timing: it must run before the
_apisix_proxiedmarker, because before_proxy plugins may end the request directly withcore.response.exit(). fetch_ctxmust clear$ctx_ref(L254): prevents residual variables from causing erroneous reuse.