APISIX init.lua Source Code Analysis

Categories: APISIX

Read in English

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:

  1. Process lifecycle hooks: initialization and cleanup for the init / init_worker / exit_worker phases;
  2. Request lifecycle orchestration: invoking the routing, plugin, and load-balancing modules in sequence across Nginx phases (accessbalancerheader_filterbody_filterlog);
  3. 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 uses core.response.exit(...) while Stream uses ngx_exit(...).
  • apisix_base_flags (L77 ~ L80): resty.apisix.patch is APISIX’s customized C module (apisix-base); a pcall loading failure degrades to an empty table. A typical use is the client_cert_verified_in_handshake flag at L402 — when the newer apisix-base has already completed client certificate verification during the handshake, there is no need to check $ssl_client_verify again.
  • apisix_ngx_client (L86): from resty.apisix.client, provides enable_mirror() (used at L880 for gRPC traffic mirroring).
  • ver_header (L84): the response Server header, degraded to "APISIX" when enable_server_tokens=false (L162 ~ L164).

4. HTTP Subsystem

4.1 http_init (L91 ~ L110) — init Phase

  1. Initialize the DNS resolver, ID generator, and environment variables;
  2. Enable the privileged agent process (for privileged operations such as dynamically setting upstreams);
  3. Initialize the config center (core.config.init, etcd / standalone yaml);
  4. xrpc.init().

4.2 http_init_worker (L113 ~ L171) — init_worker Phase

Initializes each subsystem in order, the order reflecting their dependencies:

  1. 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);
  2. Event system, lrucache, service discovery, balancer, admin, background timers, debug;
  3. Worker-level initialization of the config center;
  4. Plugin system: plugin → router → service → plugin_config → consumer → consumer_group → secret → global_rules → upstream → ext-plugin;
  5. Control API router, local_conf cache, Server header version;
  6. Prometheus plugin initialization (must come last so that metrics of all workers are ready);
  7. 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_request extension;
  • Take api_ctx from 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_protocols according 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 a client certificate 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):
    1. Support skip_mtls_uri_regex: exempt matching URIs from client certificates (L363 ~ L368, L381);
    2. Certificate verification (preferentially trusting the handshake-time verification flag from apisix-base, L402);
    3. SNI vs Host consistency check (L417 ~ L426): prevent cross-domain certificate reuse where a user configures a *.domain wildcard certificate but accesses with SNI a.domain while carrying Host b.domain;
    4. Session-resumption check.

4.4 http_access_phase (L691 ~ L857) — Core of the access Phase

Execution order:

  1. HTTP/3 adaptation (L694 ~ L696): when ngx.req.http_version() == 3, convert the :authority pseudo-header into upstream_host;
  2. api_ctx creation (L700 ~ L704): take from tablepool, attach to ngx.ctx, and initialize the var metatable (lazily reads nginx variables);
  3. mTLS verification (L708);
  4. Dynamic debugging (L712);
  5. 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, %2e encoded dot segments);
  6. request_uri anti-injection rewrite (L740 ~ L741): only the normalized URI is written into request_uri; the original value is stored in real_request_uri;
  7. X-Forwarded-* anti-spoofing (L743, see §5.4);
  8. Route matching (L745 ~ L759): router.router_http.match(api_ctx); on a miss → still run global rules → return 404;
  9. Config merging (L767 ~ L802): plugin_configservice (merge_service_route), set context fields such as conf_type / conf_version / conf_id / route_id;
  10. Run global rules (L807 ~ L808);
  11. Plugin/script execution (L810 ~ L851):
    • Script routes: directly script.run("access");
    • Plugin routes: plugin.filter filters out the plugins active on this route → run the rewrite phase first → if the rewrite phase produced a consumer (e.g., key-auth succeeded), merge the consumer / consumer_group config; if the config changed, re-filter and additionally run the rewrite_in_consumer phase → finally run the access phase;
  12. handle_upstream (L854);
  13. Set upstream X-Forwarded-* headers (L856).

4.5 handle_upstream (L493 ~ L610)

  1. 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 the before_proxy plugin phase (L495 ~ L498);
  2. Determine the upstream: upstream_id / traffic-split overridden upstream_id (L500 ~ L535); routes with domain nodes (has_domain) trigger parse_domain_in_route (L258 ~ L287) for DNS resolution, maintaining node versions via _nodes_ver and the resource module;
  3. Upstream mTLS (L537 ~ L558): upstream.tls.client_cert_id fetches the client certificate from router_ssl;
  4. WebSocket (L560 ~ L564): pass through the Upgrade / Connection headers;
  5. kafka upstream: handed over to pubsub_kafka.access (L569 ~ L571);
  6. set_upstream sets proxy parameters → load_balancer.pick_server selects a node (L573 ~ L587);
  7. set_upstream_headers (L587): set the Host according to pass_host semantics (pass/rewrite/node, L290 ~ L308);
  8. before_proxy runs early (L590): the before_proxy plugins run in the access phase; the comment explains that this is to “avoid always reinitializing the request”;
  9. _apisix_proxied marker (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”;
  10. gRPC / Dubbo dispatch (L600 ~ L609): stash_ngx_ctx() then ngx.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(): restores ngx.ctx in the target location by reference number, and clears the variable.

4.7 Response and Log Phases

  • http_header_filter_phase (L910 ~ L938):
    • Sets the Server header;
    • X-APISIX-Upstream-Status: shows the upstream status code per configuration, by default only on 5xx (L885 ~ L907);
    • Runs the header_filter plugin phase;
    • Outputs the debug header Apisix-Plugins (L927 ~ L934);
    • Pre-creates the span for the body_filter phase at the end.
  • http_body_filter_phase (L941 ~ L944): runs the body_filter and delayed_body_filter plugin phases.
  • http_log_phase (L1088 ~ L1123):
    1. End all tracer spans;
    2. Fallback for apisix_upstream_response_time;
    3. Run the log plugin phase;
    4. Passive health check reporting (L1104, §4.8);
    5. after_balance hook;
    6. Unified resource release: release_vars, release the plugins / matched_route_record / api_ctx tablepool 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; the status-report shdict 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 implement check_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:

  1. 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 7239 Forwarded header;
    • Sync the var.http_x_forwarded_* cache.
  2. set_upstream_x_forwarded_headers (end of access): write the trusted values back into the var_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, calls enable_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)

  1. mTLS verification (L1262, verify_tls_client);
  2. Route matching router_stream.match;
  3. Upstream determination: three sources — upstream_id / service_id (merge_service_stream_route) / route inline upstream (including has_domain DNS resolution);
  4. plugin.stream_filter filters plugins → run the preread plugin phase;
  5. Protocol dispatch (L1350 ~ L1353): when protocol is configured, hand over to xrpc.run_protocol (e.g., Redis, Kafka protocol plugins); otherwise, direct TCP passthrough;
  6. TCP direct path: set_upstream → pick_server → before_proxy.

7.3 Subsequent Phases

  • stream_balancer_phase (L1374): same as HTTP, calls load_balancer.run;
  • stream_log_phase (L1386): log plugins → passive health check → release api_ctx (note that, unlike HTTP, it directly does plugin.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_ctx is fetched from the pool in the access phase and returned in the log phase (L1122 / L1401); same for plugins and matched_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 the var metatable; 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_route caches resolution results via resource.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

  1. 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 only ngx_exit(1).
  2. local_conf gets refreshed repeatedly: cached once in http_init_worker (L160), but set_resp_upstream_status, cors_admin, and status_ready call core.config.local_conf() again — deliberate (config may be hot-updated in standalone mode).
  3. The api_ctx in ssl_client_hello_phase is returned right after use (L214 ~ L215): it is not the same api_ctx as in the access phase and cannot be reused.
  4. before_proxy timing: it must run before the _apisix_proxied marker, because before_proxy plugins may end the request directly with core.response.exit().
  5. fetch_ctx must clear $ctx_ref (L254): prevents residual variables from causing erroneous reuse.
Read More

Easegress Source Code Reading Guide

【2026-07-31】A systematic guide to reading Easegress source code - from project overview, core architecture, and object system to the traffic pipeline