What this post covers
This is the fifth post in the "Digging into nginx's Source" series. In part 4 we followed main() into ngx_init_cycle(), where ngx_conf_parse() chops nginx.conf into words (tokens). This post picks up right after that: we follow a line such as greet_message "hi"; and see which module's function it reaches, and which field of which struct it ends up in.
Four questions:
- What is a "module" in the source, and who decides which modules are in the binary?
- Given the name
sendfile on;, how does nginx find a function inngx_http_core_module? What separates "unknown directive" from "is not allowed here"? - Every
http {},server {}andlocation {}gets its own copy of a module's config struct — how many, and when are they created? - When does the merge happen, the step that lets a value written in
http {}reach alocation {}? On every request?
| Item | Details |
|---|---|
| Target | nginx |
| Version | nginx 1.31.9 (master, development) |
| Commit | 9a7439fd79 (2026-10-23) |
| Main files | src/core/ngx_module.[ch], src/core/ngx_conf_file.[ch], src/core/nginx.c (load_module), auto/modules, src/http/ngx_http_config.h, src/http/ngx_http.c, src/http/ngx_http_core_module.c |
All line numbers refer to this commit. Part 1 shows how to check out the same commit and build the Docker image used for the examples. The config and module code repeats itself in the same shape for events, stream and mail besides HTTP, so this post sticks to the main path through core and HTTP. What I left out is listed by name and location in "Going deeper" at the end.
On the map from part 2, this post zooms into the modules·directives box in zone A. Preparation.
The blue box is this post. Part 4 is about "reading" the config file; this post is about "handing" what was read to the modules.
Terms to know first
A module is the unit of almost every feature in nginx. HTTP processing itself (ngx_http_core_module), static files (ngx_http_static_module), proxying (ngx_http_proxy_module) — even the events {} block and the error_log directive are modules. In the source, a module is one struct variable of type ngx_module_t. Not a class or a plugin object, just a C struct holding function pointers and tables. If C structs and function pointers feel rusty, httpd part 2: Before Reading a Server Written in C — Structs, Function Pointers, Macros is a good warm-up.
The module type is the type field of ngx_module_t: NGX_CORE_MODULE, NGX_EVENT_MODULE, NGX_HTTP_MODULE, NGX_STREAM_MODULE, NGX_MAIL_MODULE and so on. The values are four-letter strings such as "CORE", "EVNT" and "HTTP" read as integers (we saw this in part 2).
The module context is the callback struct that the ctx field of ngx_module_t points to, and its shape depends on the type. For an HTTP module it is ngx_http_module_t (8 callbacks); for a core module it is ngx_core_module_t (2 callbacks). "ctx" means several different things in nginx's source, which is confusing; in this post I call this one the "module context" and the ngx_http_conf_ctx_t below the "config context struct".
A directive is one command line (sendfile on;) or block (server { … }) in nginx.conf. In the source it is one element of the ngx_command_t array a module exposes: a name, bit flags for where it may appear and how many arguments it takes, a function that handles the value (set), and where to put the value (conf, offset).
A config context is where a directive sits — main (top level of the file), events {}, http {}, server {}, location {} and so on. An HTTP module can keep a separate config struct per context; the three kinds are called main_conf, srv_conf and loc_conf.
index and ctx_index: every module has two numbers. index is its number among all modules; ctx_index is its number among modules of the same type. The first is the slot number in the cycle-wide config array; the second is the slot number in HTTP's main_conf[], srv_conf[] and loc_conf[] arrays.
Merging is what lets a lower context inherit a value written in a higher one. Write sendfile on; in http {} and sendfile is on in every location {} below it — thanks to merging. Each module implements it itself in callbacks such as merge_loc_conf().
A dynamic module is not built into the nginx executable; it is built as a separate .so file and read with load_module in the config file. The example module in this post is one.
cycle and phase were explained in earlier parts. The cycle (ngx_cycle_t) is the struct that holds the state of a running nginx — the config it read, the module list, the connection array — and a new one is built on every reload (part 2). A phase is one of the steps nginx splits the handling of an HTTP request into (access checks, producing content, …); modules join request handling by registering a function for a phase (part 9).
1. A module is one struct — ngx_module_t
struct ngx_module_s {
ngx_uint_t ctx_index;
ngx_uint_t index;
char *name;
ngx_uint_t spare0;
ngx_uint_t spare1;
ngx_uint_t version;
const char *signature;
void *ctx;
ngx_command_t *commands;
ngx_uint_t type;
ngx_int_t (*init_master)(ngx_log_t *log);
ngx_int_t (*init_module)(ngx_cycle_t *cycle);
ngx_int_t (*init_process)(ngx_cycle_t *cycle);
ngx_int_t (*init_thread)(ngx_cycle_t *cycle);
void (*exit_thread)(ngx_cycle_t *cycle);
void (*exit_process)(ngx_cycle_t *cycle);
void (*exit_master)(ngx_cycle_t *cycle);
/* ... spare_hook0 ~ spare_hook7 ... */
};
src/core/ngx_module.h lines 223–258
The fields fall into three groups.
- Numbers and labels —
ctx_index,index,name, plusversionandsignature, which are checked when a dynamic module is loaded (section 2). - What the module does —
type,ctx(the type-specific callback struct) andcommands(the directive table). These are the stars of this post. - Process lifetime callbacks —
init_module(in the master, after the config is read),init_process(when a worker starts),exit_processand friends. The master and workers come in part 6.
The key point is that ctx is a void *. Different module types need different callbacks, so ngx_module_t only says "this points at something"; the code that knows the type (ngx_http.c for HTTP modules) looks at type and reinterprets it as ngx_http_module_t *. An HTTP module's context looks like this:
typedef struct {
ngx_int_t (*preconfiguration)(ngx_conf_t *cf);
ngx_int_t (*postconfiguration)(ngx_conf_t *cf);
void *(*create_main_conf)(ngx_conf_t *cf);
char *(*init_main_conf)(ngx_conf_t *cf, void *conf);
void *(*create_srv_conf)(ngx_conf_t *cf);
char *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);
void *(*create_loc_conf)(ngx_conf_t *cf);
char *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf);
} ngx_http_module_t;
src/http/ngx_http_config.h lines 24–36
create_* means "make me an empty config struct and return it", merge_* means "pass the parent's (prev) values down to the child (conf)", and init_main_conf means "the http block has been read; fill the blanks in the main config with defaults". preconfiguration and postconfiguration are each called once, right before and right after the http block is read. When these eight are called is the subject of the diagram in section 5.
One module = one struct. Blue boxes are what this post follows; the grey box holds the process lifetime callbacks covered in part 6.
The smallest real module: empty_gif
One of the tiniest modules shipped with nginx is ngx_http_empty_gif_module (used as location = /pixel.gif { empty_gif; }, it returns a 1×1 transparent GIF). It has one directive, and all eight callback slots in its context are NULL. Its directive handler:
static char *
ngx_http_empty_gif(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_core_loc_conf_t *clcf;
clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
clcf->handler = ngx_http_empty_gif_handler;
return NGX_CONF_OK;
}
src/http/modules/ngx_http_empty_gif_module.c lines 131–140
It has no config struct of its own, so it takes someone else's — the core module's loc_conf of the location {} being read — and plugs its own function into the handler function pointer. Requests that land in this location get their response from that function in the content phase. proxy_pass (ngx_http_proxy_pass()) and fastcgi_pass install their content handlers the same way. Where handler is called at request time is shown with the content phase in part 9.
The module variable itself (ngx_module_t ngx_http_empty_gif_module = { NGX_MODULE_V1, &ngx_http_empty_gif_module_ctx, ngx_http_empty_gif_commands, NGX_HTTP_MODULE, NULL, … }) is at lines 93–106 of the same file — it fills in the context, the directive table and the type, and leaves the seven process lifetime callbacks NULL.
Easy to trip on: NGX_MODULE_V1 fills seven fields at once
The first line of the initializer, NGX_MODULE_V1, is a macro:
#define NGX_MODULE_V1 \
NGX_MODULE_UNSET_INDEX, NGX_MODULE_UNSET_INDEX, \
NULL, 0, 0, nginx_version, NGX_MODULE_SIGNATURE
#define NGX_MODULE_V1_PADDING 0, 0, 0, 0, 0, 0, 0, 0
src/core/ngx_module.h lines 216–220
It sets ctx_index and index to "not yet" ((ngx_uint_t) -1), name to NULL, spare0 and spare1 to 0, and version and signature to the nginx version and the build-option string at the time this module was compiled. That is why module sources never write their own numbers or names — nginx assigns the numbers while starting up (section 2). The trailing NGX_MODULE_V1_PADDING zeroes the eight spare_hook0~7 slots.
2. Where the module list comes from
The array configure generates: objs/ngx_modules.c
Which modules are in nginx is decided at build time. When you run ./configure --with-http_ssl_module …, the shell script auto/modules collects the enabled module names in order and writes a C file, objs/ngx_modules.c:
echo >> $NGX_MODULES_C
echo 'ngx_module_t *ngx_modules[] = {' >> $NGX_MODULES_C
for mod in $modules
do
echo " &$mod," >> $NGX_MODULES_C
done
$modules is the list assembled on lines 1490–1572 of the same file in the order core → event → (thread pool) → HTTP → mail → stream → misc. The result is the array ngx_modules[] of module variable addresses and the array ngx_module_names[] of their names. The order of this array is the order of the modules. Directive lookup (section 4) and callback calls both walk it in this order. How this decides the order of filter modules is covered in part 10.
At startup: numbering
Right before calling ngx_init_cycle(), main() walks this array once:
ngx_int_t
ngx_preinit_modules(void)
{
ngx_uint_t i;
for (i = 0; ngx_modules[i]; i++) {
ngx_modules[i]->index = i;
ngx_modules[i]->name = ngx_module_names[i];
}
ngx_modules_n = i;
ngx_max_module = ngx_modules_n + NGX_MAX_DYNAMIC_MODULES;
return NGX_OK;
}
src/core/ngx_module.c lines 25–39
A static module's index is simply its position in the array. ngx_max_module is the number of static modules plus 128 slots for dynamic modules (NGX_MAX_DYNAMIC_MODULES, line 13), and the cycle's array of config pointers, conf_ctx, is created with that size (ngx_cycle.c line 200). Room is reserved up front so the array never has to be rebuilt when dynamic modules join later.
Next, ngx_cycle_modules() (ngx_module.c lines 42–62) inside ngx_init_cycle() copies ngx_modules[] into cycle->modules, separately for each cycle. The global array is not used directly because of dynamic modules: when a reload builds a new cycle, load_module lines may have been added or removed, so the module list can differ (why a new cycle is built is in part 4).
load_module: inserting a dynamic module
load_module is a directive of the core module ngx_core_module, handled by ngx_load_module():
if (cf->cycle->modules_used) {
return "is specified too late";
}
/* ... */
handle = ngx_dlopen(file.data);
/* ... */
modules = ngx_dlsym(handle, "ngx_modules");
/* ... */
order = ngx_dlsym(handle, "ngx_module_order");
for (i = 0; modules[i]; i++) {
module = modules[i];
module->name = names[i];
if (ngx_add_module(cf, &file, module, order) != NGX_OK) {
return NGX_CONF_ERROR;
}
ngx_log_debug2(NGX_LOG_DEBUG_CORE, cf->log, 0, "module: %s i:%ui",
module->name, module->index);
}
src/core/nginx.c lines 1663–1719
It opens the .so with dlopen() and uses dlsym() to find an array with the very same name, ngx_modules inside it. That works because configure also generates a small ngx_modules.c for each dynamic module's .so (we'll see it in example 1). Each module in it is checked by ngx_add_module() and inserted into cycle->modules:
if (module->version != nginx_version) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"module \"%V\" version %ui instead of %ui",
file, module->version, (ngx_uint_t) nginx_version);
return NGX_ERROR;
}
if (ngx_strcmp(module->signature, NGX_MODULE_SIGNATURE) != 0) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"module \"%V\" is not binary compatible",
file);
return NGX_ERROR;
}
src/core/ngx_module.c lines 170–182
It compares the version and signature that NGX_MODULE_V1 baked into the module with those of the running nginx. The signature (NGX_MODULE_SIGNATURE, ngx_module.h lines 201–213) is three numbers such as pointer size followed by 34 0/1 characters. Most of the 34 are build conditions — "is epoll available", "are threads enabled", "is SSL enabled" — and 7 are fixed constants. These conditions change the shape of nginx's structs (which fields exist), so a mismatch would mean reading the same struct differently. That is why a dynamic module must be built for the same nginx version with the same configure options, and why the example reuses the configure arguments printed by nginx -V.
Once it passes, the module receives the smallest unused number as its index (ngx_module_index(), lines 279–315) and is placed in front of a module named in order — order matters for filter modules, so configure also generates ngx_module_order[] — or, with no order, appended at the end (lines 211–252).
ctx_index is assigned when the http block is reached
Unlike index, ctx_index is not assigned early in startup. When http { is reached and ngx_http_block() starts, ngx_count_modules(cf->cycle, NGX_HTTP_MODULE) counts only the HTTP modules and numbers them 0, 1, 2, …:
for (i = 0; cycle->modules[i]; i++) {
module = cycle->modules[i];
if (module->type != type) {
continue;
}
/* ... a module that already has a ctx_index (recounting after a reload) keeps it ... */
/* search for some free index */
module->ctx_index = ngx_module_ctx_index(cycle, type, next);
if (module->ctx_index > max) {
max = module->ctx_index;
}
next = module->ctx_index + 1;
}
/* ... the old cycle's module numbers are also taken into max ... */
/* prevent loading of additional modules */
cycle->modules_used = 1;
return max + 1;
src/core/ngx_module.c lines 93–152
The return value max + 1 becomes ngx_http_max_module, the number of slots in each HTTP config array. At the end, cycle->modules_used = 1 closes the door. That flag is exactly what the first check in ngx_load_module() looked at. This is why load_module must come before blocks such as http {} and events {}: once a block starts being read, the modules have been counted and the array sizes fixed, so a module added afterwards wouldn't fit. Event modules are counted at events { (ngx_event.c line 971); stream and mail call the same function in their own blocks.
The yellow box is a file produced by the build, blue boxes are functions that run during startup, and the red box is what happens when the order is broken.
Example 1: building a dynamic module and looking at the module list
For this post I wrote a small module called ngx_http_greet_module — a port to nginx of the mod_greet module from httpd part 7. It has four directives:
| Directive | Arguments | Allowed in | Stored in | Handler |
|---|---|---|---|---|
greet |
none | location | (replaces core's clcf->handler) |
hand-written ngx_http_greet() |
greet_message |
1 | http, server, location | message in loc_conf |
ngx_conf_set_str_slot |
greet_shout |
on/off | http, server, location | shout in loc_conf |
ngx_conf_set_flag_slot |
greet_tag |
1 | http, server | tag in srv_conf |
ngx_conf_set_str_slot |
A request to a location with greet; prints the values that apply to that request and "which line of nginx.conf the config struct was created on". The create and merge callbacks log one line each time they are called. Here are the module description file (config) and the module source:
# Example written for this post (not part of the nginx source)
# module description file that configure reads via --add-dynamic-module=/work
ngx_addon_name=ngx_http_greet_module
ngx_module_type=HTTP
ngx_module_name=ngx_http_greet_module
ngx_module_srcs="$ngx_addon_dir/ngx_http_greet_module.c"
. auto/module
/* Example written for this post (not part of the nginx source) */
/* ... header comment describing the four directives ... */
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
typedef struct {
ngx_uint_t line; /* config file line being read when this struct was created */
ngx_str_t message; /* greet_message — data == NULL means "not written" */
ngx_flag_t shout; /* greet_shout — NGX_CONF_UNSET means "not written" */
} ngx_http_greet_loc_conf_t;
/* ... ngx_http_greet_srv_conf_t (line, tag), function declarations ... */
static ngx_command_t ngx_http_greet_commands[] = {
{ ngx_string("greet"),
NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS,
ngx_http_greet,
0,
0,
NULL },
{ ngx_string("greet_message"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_str_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_greet_loc_conf_t, message),
NULL },
{ ngx_string("greet_shout"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_greet_loc_conf_t, shout),
NULL },
{ ngx_string("greet_tag"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
ngx_conf_set_str_slot,
NGX_HTTP_SRV_CONF_OFFSET,
offsetof(ngx_http_greet_srv_conf_t, tag),
NULL },
ngx_null_command
};
/* ... ngx_http_greet_module_ctx: only postconfiguration, create/merge_srv_conf, create/merge_loc_conf are set ... */
/* ... ngx_module_t ngx_http_greet_module = { NGX_MODULE_V1, ... } — same shape as empty_gif ... */
/* ... ngx_http_greet_handler(), ngx_http_greet() — installs the handler the same way as empty_gif ... */
/* ... postconfiguration, srv_conf create/merge — same shape as the loc_conf ones below ... */
static void *
ngx_http_greet_create_loc_conf(ngx_conf_t *cf)
{
ngx_http_greet_loc_conf_t *conf;
conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_greet_loc_conf_t));
if (conf == NULL) {
return NULL;
}
conf->line = cf->conf_file->line;
conf->shout = NGX_CONF_UNSET;
ngx_conf_log_error(NGX_LOG_NOTICE, cf, 0, "greet create_loc_conf");
return conf;
}
static char *
ngx_http_greet_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
{
ngx_http_greet_loc_conf_t *prev = parent;
ngx_http_greet_loc_conf_t *conf = child;
ngx_conf_log_error(NGX_LOG_NOTICE, cf, 0,
"greet merge_loc_conf: line %ui -> line %ui, "
"message %s, shout %s",
prev->line, conf->line,
conf->message.data ? "set here" : "inherited",
conf->shout != NGX_CONF_UNSET ? "set here" : "inherited");
ngx_conf_merge_str_value(conf->message, prev->message, "hello");
ngx_conf_merge_value(conf->shout, prev->shout, 0);
return NGX_CONF_OK;
}
ngx_conf_log_error() appends "the config file and line currently being read" (in /work/nginx.conf:12) to the message (ngx_conf_file.c lines 1025–1027), which is perfect for seeing when a callback ran.
The build runs inside the container with the installed nginx's configure arguments plus --add-dynamic-module, followed by make modules. The relevant part of run.sh and its output:
cd /usr/src/nginx
# same configure arguments as the installed nginx + --add-dynamic-module (load_module accepts it only if the signature matches)
eval ./auto/configure $(nginx -V 2>&1 | sed -n 's/^configure arguments: //p') \
--add-dynamic-module=/work > /tmp/u05/configure.log 2>&1
make modules > /tmp/u05/make.log 2>&1
=== 1. build the dynamic module (configure + make modules) ===
72152 objs/ngx_http_greet_module.so
signature: 8,4,8,0010111111010111001111111111100110
=== 2. objs/ngx_http_greet_module_modules.c (generated by configure) ===
#include <ngx_config.h>
#include <ngx_core.h>
extern ngx_module_t ngx_http_greet_module;
ngx_module_t *ngx_modules[] = {
&ngx_http_greet_module,
NULL
};
char *ngx_module_names[] = {
"ngx_http_greet_module",
NULL
};
char *ngx_module_order[] = {
NULL
};
=== 3. /opt/nginx/ngx_modules.c (static modules of this build) ===
extern lines: 89
entries in ngx_modules[]: 89
1 &ngx_core_module,
2 &ngx_errlog_module,
3 &ngx_conf_module,
4 &ngx_openssl_module,
5 &ngx_openssl_cache_module,
6 &ngx_regex_module,
7 &ngx_events_module,
8 &ngx_event_core_module,
11 &ngx_http_module,
12 &ngx_http_core_module,
16 &ngx_http_static_module,
34 &ngx_http_proxy_module,
69 &ngx_stream_module,
89 &ngx_stream_upstream_zone_module,
I ran this example in a Docker container (Debian bookworm, --with-debug) with nginx 1.31.9 built unmodified from the reference commit (9a7439fd79). The .so size may differ with the build environment.
- The signature string is inside the
.so.8,4,8,are the sizes of a pointer,sig_atomic_tandtime_t; the 34 digits after them are the build conditions (7 of them fixed constants). Built with the same configure arguments as the installed nginx, it passes thestrcmpinngx_add_module(). - A dynamic module's
.sohas its ownngx_modules[],ngx_module_names[]andngx_module_order[]. This is theobjs/<module>_modules.cfile thatauto/makelines 493–564 write for each module. It's a plain HTTP module, not a filter, songx_module_order[]is empty and the module goes at the end. - This build has 89 static modules. Core and event modules come first, then
ngx_http_moduleat #11 (a core-type module — it owns just thehttp {}block directive), HTTP modules fromngx_http_core_moduleat #12, and stream modules from #69.
Easy to trip on: ngx_http_module and ngx_http_core_module are different modules
The names are close, but ngx_http_module has type NGX_CORE_MODULE and a single directive, http (ngx_http.c lines 86–119). It has to be a core module to recognize http { at the top level (main context). ngx_http_core_module has type NGX_HTTP_MODULE and owns HTTP's basic directives such as server, location, listen, root and sendfile. events is split the same way into ngx_events_module (core type, the events block) and ngx_event_core_module (event type, worker_connections and so on). One module opens the outer block and another fills it in.
3. The directive table — ngx_command_t
struct ngx_command_s {
ngx_str_t name;
ngx_uint_t type;
char *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
ngx_uint_t conf;
ngx_uint_t offset;
void *post;
};
#define ngx_null_command { ngx_null_string, 0, NULL, 0, 0, NULL }
src/core/ngx_conf_file.h lines 77–86
| Field | Meaning | Value for greet_message |
|---|---|---|
name |
directive name (ngx_str_t — length plus pointer) |
"greet_message" |
type |
allowed-context bits + argument-count bits | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1 |
set |
function that handles the value | ngx_conf_set_str_slot |
conf |
which array (main/srv/loc) of the context struct to use — a byte offset | NGX_HTTP_LOC_CONF_OFFSET |
offset |
which field of the module's config struct to write — a byte offset | offsetof(ngx_http_greet_loc_conf_t, message) |
post |
optional check function called after the value is stored | NULL |
The table ends with ngx_null_command (an element whose name length is 0); lookups walk until cmd->name.len is 0.
type — one bit, one meaning
/*
* AAAA number of arguments
* FF command flags
* TT command type, i.e. HTTP "location" or "server" command
*/
#define NGX_CONF_NOARGS 0x00000001
#define NGX_CONF_TAKE1 0x00000002
#define NGX_CONF_TAKE2 0x00000004
/* ... TAKE3 ~ TAKE7, combinations such as TAKE12 ... */
#define NGX_CONF_ARGS_NUMBER 0x000000ff
#define NGX_CONF_BLOCK 0x00000100
#define NGX_CONF_FLAG 0x00000200
#define NGX_CONF_ANY 0x00000400
#define NGX_CONF_1MORE 0x00000800
#define NGX_CONF_2MORE 0x00001000
#define NGX_DIRECT_CONF 0x00010000
#define NGX_MAIN_CONF 0x01000000
#define NGX_ANY_CONF 0xFF000000
src/core/ngx_conf_file.h lines 16–52
One 32-bit integer is split into three parts. The lowest two hex digits are the argument count — TAKE1 means "exactly 1", and ORing bits collects the allowed counts, e.g. TAKE12 for "1 or 2". Above that is the shape — BLOCK (a block opened with {), FLAG (on/off), 1MORE (one or more). The top two hex digits are the context. HTTP's context bits are in ngx_http_config.h:
#define NGX_HTTP_MODULE 0x50545448 /* "HTTP" */
#define NGX_HTTP_MAIN_CONF 0x02000000
#define NGX_HTTP_SRV_CONF 0x04000000
#define NGX_HTTP_LOC_CONF 0x08000000
#define NGX_HTTP_UPS_CONF 0x10000000
#define NGX_HTTP_SIF_CONF 0x20000000
#define NGX_HTTP_LIF_CONF 0x40000000
#define NGX_HTTP_LMT_CONF 0x80000000
#define NGX_HTTP_MAIN_CONF_OFFSET offsetof(ngx_http_conf_ctx_t, main_conf)
#define NGX_HTTP_SRV_CONF_OFFSET offsetof(ngx_http_conf_ctx_t, srv_conf)
#define NGX_HTTP_LOC_CONF_OFFSET offsetof(ngx_http_conf_ctx_t, loc_conf)
src/http/ngx_http_config.h lines 39–52
UPS is inside upstream {}, SIF and LIF are inside an if () {} in server or location, and LMT is inside limit_except {}. sendfile, for example, is MAIN|SRV|LOC|LIF|FLAG, so it is also allowed in an if block inside a location (ngx_http_core_module.c lines 404–410). The "Context: http, server, location, if in location" line in the nginx docs is these bits translated into words.
One type field holds the context, the shape and the argument count together. The values are the #defines in ngx_conf_file.h lines 16–52 and ngx_http_config.h lines 39–52.
Easy to trip on: 0x02000000 means three things
NGX_HTTP_MAIN_CONF (0x02000000) has the same value as NGX_EVENT_CONF (ngx_event.h line 397), NGX_STREAM_MAIN_CONF (ngx_stream.h line 317) and NGX_MAIL_MAIN_CONF (ngx_mail.h line 357). The top eight bits are reused by each type. So the context bits alone can't tell "inside http" from "inside events", and ngx_conf_handler() in section 4 compares the module type before checking the bits. The first error in example 3 is exactly this case.
Standard setters — most directives don't write a new function
A module may write its own set function, but common shapes — "one string", "on/off", "a number", "a size (8k)", "a time (60s)" — use functions core already provides, the slot functions (12 of them on lines 284–296 of ngx_conf_file.h). The one for on/off:
char *
ngx_conf_set_flag_slot(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
char *p = conf;
ngx_str_t *value;
ngx_flag_t *fp;
ngx_conf_post_t *post;
fp = (ngx_flag_t *) (p + cmd->offset);
if (*fp != NGX_CONF_UNSET) {
return "is duplicate";
}
value = cf->args->elts;
if (ngx_strcasecmp(value[1].data, (u_char *) "on") == 0) {
*fp = 1;
} else if (ngx_strcasecmp(value[1].data, (u_char *) "off") == 0) {
*fp = 0;
} else {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid value \"%s\" in \"%s\" directive, "
"it must be \"on\" or \"off\"",
value[1].data, cmd->name.data);
return NGX_CONF_ERROR;
}
if (cmd->post) {
post = cmd->post;
return post->post_handler(cf, post, fp);
}
return NGX_CONF_OK;
}
src/core/ngx_conf_file.c lines 1031–1068
This function has no idea which module or which struct it is writing to. It takes the struct's start address (conf), adds cmd->offset bytes, treats the result as an ngx_flag_t and writes the value. The struct's layout reaches it only as a single number stored with offsetof() in the offset field of ngx_command_t. That's how sendfile, tcp_nopush, gzip and the example's greet_shout all share this one function.
Also note the line that returns "is duplicate" when *fp != NGX_CONF_UNSET. The value must still be "not set" (-1) to be written. So create_loc_conf() must initialize flag fields to NGX_CONF_UNSET — the example's conf->shout = NGX_CONF_UNSET; does that. Leave it at 0 and you can't tell "explicitly off" from "not written", and the merge in section 6 breaks too. For strings, data == NULL means "not written", so the zero from ngx_pcalloc() is enough (ngx_conf_set_str_slot, the if (field->data) on lines 1071–1095).
When a set function returns a string, that string becomes the tail of the error message: as section 4 shows, ngx_conf_handler() prefixes it with the directive name using "\"%s\" directive %s".
4. Finding a function by name — ngx_conf_handler()
ngx_conf_parse(), which we saw in part 4, collects the words of one line into the cf->args array with ngx_conf_read_token(), and calls ngx_conf_handler() when it hits ; or { (ngx_conf_file.c line 325). The name lookup:
for (i = 0; cf->cycle->modules[i]; i++) {
cmd = cf->cycle->modules[i]->commands;
if (cmd == NULL) {
continue;
}
for ( /* void */ ; cmd->name.len; cmd++) {
if (name->len != cmd->name.len) {
continue;
}
if (ngx_strcmp(name->data, cmd->name.data) != 0) {
continue;
}
found = 1;
if (cf->cycle->modules[i]->type != NGX_CONF_MODULE
&& cf->cycle->modules[i]->type != cf->module_type)
{
continue;
}
/* is the directive's location right ? */
if (!(cmd->type & cf->cmd_type)) {
continue;
}
src/core/ngx_conf_file.c lines 374–403
No hash, no tree: it compares against every directive of every module in turn. Comparing lengths first filters out most of them. This lookup only runs while reading the config at startup and reload; it plays no part in request processing (httpd keeps a hash of directive names instead — httpd part 7).
When the name matches, it records found = 1 and checks two things:
- Module type:
cf->module_typesays "which type of block are we in". At the top level it isNGX_CORE_MODULE; insidehttp {it isNGX_HTTP_MODULE(ngx_http_block()in section 5 changes it). The exception isNGX_CONF_MODULE—ngx_conf_module, which owns just theincludedirective — which passes in any block. - Context bits:
cf->cmd_typesays "which level inside the block" (NGX_HTTP_MAIN_CONF,_SRV_CONF,_LOC_CONF…), and that bit must be set in the directive'stype.
If either fails, continue — a different module's directive with the same name may exist, so the search goes on. If nothing fits by the end, found decides between two errors (lines 486–496): no such name at all gives unknown directive; the name exists but not for this place gives is not allowed here.
After that come the argument-count checks (lines 405–449, using the argument_number[] table to map "n arguments → the TAKEn bit"), and then the conf pointer to pass to set:
/* set up the directive's configuration context */
conf = NULL;
if (cmd->type & NGX_DIRECT_CONF) {
conf = ((void **) cf->ctx)[cf->cycle->modules[i]->index];
} else if (cmd->type & NGX_MAIN_CONF) {
conf = &(((void **) cf->ctx)[cf->cycle->modules[i]->index]);
} else if (cf->ctx) {
confp = *(void **) ((char *) cf->ctx + cmd->conf);
if (confp) {
conf = confp[cf->cycle->modules[i]->ctx_index];
}
}
rv = cmd->set(cf, cmd, conf);
src/core/ngx_conf_file.c lines 451–469
The three red boxes are the error messages you see most often from nginx -t. Example 3 triggers them one by one.
Three ways to find the conf pointer
cf->ctx is "the config pointers of the current context". At the top level it is cycle->conf_ctx (a void * array indexed by module index, ngx_cycle.c line 269); inside http { it is an ngx_http_conf_ctx_t.
NGX_DIRECT_CONF— ordinary core module directives (worker_processes,pid,load_module…).ngx_init_cycle()calledcreate_conf()for every core module before reading the config and stored the structs inconf_ctx[index](ngx_cycle.c lines 233–248), so the struct is handed over directly.NGX_MAIN_CONFwithout DIRECT — block directives such ashttp,eventsandstream. Instead of a struct, it passes the address of the slotconf_ctx[index]. The slot is still empty; the block handler creates its own context struct and plugs it into the slot.*(ngx_http_conf_ctx_t **) conf = ctx;at the top ofngx_http_block()(ngx_http.c line 145) does exactly that.- Everything else (inside HTTP and other blocks) — it treats
cf->ctxas achar *, addscmd->confbytes to pick one of themain_conf/srv_conf/loc_conffields, and takes slotctx_indexfrom that array. SinceNGX_HTTP_LOC_CONF_OFFSETwasoffsetof(ngx_http_conf_ctx_t, loc_conf), the result isctx->loc_conf[ctx_index].
Two offsets (cmd->conf, cmd->offset) and two numbers (index, ctx_index). 57 is the ctx_index the greet module received in the example build (example 2).
ngx_conf_handler() knows nothing about HTTP. It never even names ngx_http_conf_ctx_t; it treats it as "a void ** array at some byte offset in the context struct". That's why stream and mail build their own context structs (ngx_stream_conf_ctx_t …) in the same shape and reuse this function unchanged.
Example 3: which line prints each error message?
The last part of the example's run.sh runs nginx -t on a small config, changing one spot each time. The skeleton (line 2 is events, line 7 is inside the location):
load_module /tmp/u05/ngx_http_greet_module.so;
events { (line 2 — what gets tried here) }
http {
server {
listen 127.0.0.1:8090;
location / {
(line 7 — what gets tried here)
}
}
}
(line 11 — what gets tried here)
=== 7. directive errors (nginx -t) ===
--- greet_message inside events {}
nginx: [emerg] "greet_message" directive is not allowed here in /tmp/u05/bad.conf:2
nginx: configuration file /tmp/u05/bad.conf test failed
--- greet_tag inside location {}
nginx: [emerg] "greet_tag" directive is not allowed here in /tmp/u05/bad.conf:7
nginx: configuration file /tmp/u05/bad.conf test failed
--- typo in the name
nginx: [emerg] unknown directive "greet_mesage" in /tmp/u05/bad.conf:7
nginx: configuration file /tmp/u05/bad.conf test failed
--- greet_shout maybe
nginx: [emerg] invalid value "maybe" in "greet_shout" directive, it must be "on" or "off" in /tmp/u05/bad.conf:7
nginx: configuration file /tmp/u05/bad.conf test failed
--- greet_message with two arguments
nginx: [emerg] invalid number of arguments in "greet_message" directive in /tmp/u05/bad.conf:7
nginx: configuration file /tmp/u05/bad.conf test failed
--- greet_message twice
nginx: [emerg] "greet_message" directive is duplicate in /tmp/u05/bad.conf:7
nginx: configuration file /tmp/u05/bad.conf test failed
--- greet_message without ;
nginx: [emerg] unexpected "}" in /tmp/u05/bad.conf:8
nginx: configuration file /tmp/u05/bad.conf test failed
--- load_module after http {}
nginx: [emerg] "load_module" directive is specified too late in /tmp/u05/bad.conf:11
nginx: configuration file /tmp/u05/bad.conf test failed
| What I put in | Message | Printed by |
|---|---|---|
greet_message inside events {} |
is not allowed here | the context bit is the same 0x02000000, but the module type (HTTP ≠ EVENT) fails → ngx_conf_file.c lines 393–397; message on lines 486–491 |
greet_tag inside a location |
is not allowed here | no NGX_HTTP_LOC_CONF bit → lines 401–403 |
greet_mesage (typo) |
unknown directive | no module has that name → lines 493–494 |
greet_shout maybe; |
invalid value … must be "on" or "off" | printed by ngx_conf_set_flag_slot() itself → lines 1055–1059 |
greet_message a b; |
invalid number of arguments | TAKE1 but two arguments → lines 445–448; message on lines 500–502 |
greet_message twice in one location |
is duplicate | ngx_conf_set_str_slot() returns "is duplicate" and ngx_conf_handler() adds the name on lines 479–480 |
missing ; |
unexpected "}" | never reaches a directive; caught while reading tokens → ngx_conf_read_token() lines 690–693 (part 4) |
load_module after http {} |
is specified too late | modules_used is already 1 → nginx.c lines 1663–1665 |
Some set functions return a short string (is duplicate), others print with ngx_conf_log_error() themselves and return NGX_CONF_ERROR (invalid value …). Either way the message ends with in file:line, because both go through the same ngx_conf_log_error().
Easy to trip on: forgetting load_module gives "unknown directive"
The difference between the second and third rows of the table is practical. If you use a dynamic module's directive but forget the load_module line, that module isn't in cycle->modules, found stays 0, and you get unknown directive — the same message as for a typo. is not allowed here, on the other hand, means "the module is loaded and the name is right, but the place is wrong".
5. The http block — config struct arrays per context
Now to the HTTP side. When http { is reached, ngx_http_block() is called through the second path in section 4. This one function owns HTTP configuration from start to finish.
The context struct: three pointer arrays
typedef struct {
void **main_conf;
void **srv_conf;
void **loc_conf;
} ngx_http_conf_ctx_t;
src/http/ngx_http_config.h lines 17–21
All three fields are arrays of void *. They have ngx_http_max_module slots, and slot k points to the config struct of the HTTP module whose ctx_index == k. Every module's struct has a different shape, so they are held as void * and the module that reads one casts it to its own type. The macros for that are ngx_http_conf_get_module_loc_conf(cf, module) (at config time) and ngx_http_get_module_loc_conf(r, module) (at request time) (ngx_http_config.h lines 55–66).
ngx_http_block() — create, read, merge, finish
/* count the number of the http modules and set up their indices */
ngx_http_max_module = ngx_count_modules(cf->cycle, NGX_HTTP_MODULE);
/* the http main_conf context, it is the same in the all http contexts */
ctx->main_conf = ngx_pcalloc(cf->pool,
sizeof(void *) * ngx_http_max_module);
/* ... */
/*
* the http null srv_conf context, it is used to merge
* the server{}s' srv_conf's
*/
ctx->srv_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module);
/* ... */
ctx->loc_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module);
/* ... */
for (m = 0; cf->cycle->modules[m]; m++) {
if (cf->cycle->modules[m]->type != NGX_HTTP_MODULE) {
continue;
}
module = cf->cycle->modules[m]->ctx;
mi = cf->cycle->modules[m]->ctx_index;
if (module->create_main_conf) {
ctx->main_conf[mi] = module->create_main_conf(cf);
/* ... */
}
if (module->create_srv_conf) {
ctx->srv_conf[mi] = module->create_srv_conf(cf);
/* ... */
}
if (module->create_loc_conf) {
ctx->loc_conf[mi] = module->create_loc_conf(cf);
/* ... */
}
}
src/http/ngx_http.c lines 148–217
In the http context it calls all three — create_main_conf, create_srv_conf and create_loc_conf — for every module. There is only one main config per http, so that one is obvious; the comment explains why srv and loc configs are created at the http level too — "null srv_conf … used to merge the server{}s' srv_conf's". They hold values written directly in http {}, such as sendfile on;, and serve as the merge parent of the servers.
It continues like this:
pcf = *cf;
cf->ctx = ctx;
for (m = 0; cf->cycle->modules[m]; m++) {
/* ... for each HTTP module ... */
if (module->preconfiguration) {
if (module->preconfiguration(cf) != NGX_OK) {
return NGX_CONF_ERROR;
}
}
}
/* parse inside the http{} block */
cf->module_type = NGX_HTTP_MODULE;
cf->cmd_type = NGX_HTTP_MAIN_CONF;
rv = ngx_conf_parse(cf, NULL);
src/http/ngx_http.c lines 219–240
It changes three fields of cf — ctx, module_type and cmd_type — and calls ngx_conf_parse(cf, NULL) again. A NULL file name means "keep reading the current file up to this block's }" (part 4). These three are exactly the values the module-type and context-bit checks in section 4 rely on. The old values are saved in pcf and restored at the end with *cf = pcf; (line 326) — change on the way into a block, restore on the way out: a hand-rolled stack.
server and location blocks — shared arrays and new arrays
server { is a directive of ngx_http_core_module, handled by ngx_http_core_server(). It repeats the same work one level down — it creates a new ngx_http_conf_ctx_t, shares the http one's main_conf pointer (ctx->main_conf = http_ctx->main_conf;), creates new srv_conf and loc_conf arrays, and calls create_srv_conf and create_loc_conf for every HTTP module (src/http/ngx_http_core_module.c lines 3031–3077).
Then it reads the inside of the server block after setting cf->cmd_type = NGX_HTTP_SRV_CONF; (lines 3098–3104). For location {, ngx_http_core_location() goes one more level down: it shares main_conf and srv_conf and creates only a new loc_conf:
pctx = cf->ctx;
ctx->main_conf = pctx->main_conf;
ctx->srv_conf = pctx->srv_conf;
ctx->loc_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module);
if (ctx->loc_conf == NULL) {
return NGX_CONF_ERROR;
}
for (i = 0; cf->cycle->modules[i]; i++) {
if (cf->cycle->modules[i]->type != NGX_HTTP_MODULE) {
continue;
}
module = cf->cycle->modules[i]->ctx;
if (module->create_loc_conf) {
ctx->loc_conf[cf->cycle->modules[i]->ctx_index] =
module->create_loc_conf(cf);
if (ctx->loc_conf[cf->cycle->modules[i]->ctx_index] == NULL) {
return NGX_CONF_ERROR;
}
}
}
clcf = ctx->loc_conf[ngx_http_core_module.ctx_index];
clcf->loc_conf = ctx->loc_conf;
src/http/ngx_http_core_module.c lines 3179–3205
The last two lines matter. The core module's location config (ngx_http_core_loc_conf_t) keeps its location's entire loc_conf array in clcf->loc_conf. Later, when a request is assigned to this location, moving just this one pointer into the request switches every module's location config at once (end of section 6).
Blue boxes are the ngx_http_conf_ctx_t created once per block; yellow boxes are arrays with one slot per module. The deeper the block, the fewer new arrays it creates.
So nginx.conf gets as many arrays as it has blocks. The example config (below) has http 1 + server 2 + location 4, so there are 7 loc_conf arrays, 3 srv_conf arrays and 1 main_conf array, each with as many slots as there are HTTP modules (58 in this build, counting the dynamic module). A module with create_loc_conf creates one struct per location block. It costs memory per block, but as section 6 shows, nothing is left to compute at request time.
Where does greet_message go when written in a server block?
greet_message has the NGX_HTTP_SRV_CONF bit, so it may appear in a server block, but its conf field is NGX_HTTP_LOC_CONF_OFFSET. Following the third path in section 4, when this directive is met in a server block the value goes into the server context's loc_conf[] — the server-level loc_conf that ngx_http_core_server() created — not into srv_conf. Directives you can write "in server and in location", like root and sendfile, nearly all live in loc_conf this way, and the server-level loc_conf becomes the merge parent of that server's locations. Values that exist once per server, like greet_tag or core's server_name and listen, live in srv_conf.
Example 2: the order of the callbacks
Here is the example nginx.conf, with line numbers because they appear in the log:
1 # Example written for this post (not part of the nginx source)
2 load_module /tmp/u05/ngx_http_greet_module.so;
3 worker_processes 1;
4 pid /tmp/u05/nginx.pid;
5 error_log /tmp/u05/error.log notice;
6 events { worker_connections 64; }
7
8 http {
9 access_log off;
10 greet_message "from http";
11
12 server {
13 listen 127.0.0.1:8080;
14 greet_tag "A";
15
16 location / {
17 greet;
18 }
19
20 location /loud/ {
21 greet_message "from location";
22 greet_shout on;
23 greet;
24
25 location /loud/inner/ {
26 greet;
27 }
28 }
29 }
30
31 server {
32 listen 127.0.0.1:8081;
33 greet_message "from server 2";
34
35 location / {
36 greet;
37 }
38 }
39 }
I ran nginx -t -e stderr to test the config only and get the log on the screen (why -e stderr is needed: see "Easy to trip on" below).
=== 4. nginx -t -e stderr (config-time callbacks) ===
2026/11/07 02:57:37 [notice] 2866#2866: greet create_srv_conf in /work/nginx.conf:8
2026/11/07 02:57:37 [notice] 2866#2866: greet create_loc_conf in /work/nginx.conf:8
2026/11/07 02:57:37 [notice] 2866#2866: greet create_srv_conf in /work/nginx.conf:12
2026/11/07 02:57:37 [notice] 2866#2866: greet create_loc_conf in /work/nginx.conf:12
2026/11/07 02:57:37 [notice] 2866#2866: greet create_loc_conf in /work/nginx.conf:16
2026/11/07 02:57:37 [notice] 2866#2866: greet create_loc_conf in /work/nginx.conf:20
2026/11/07 02:57:37 [notice] 2866#2866: greet create_loc_conf in /work/nginx.conf:25
2026/11/07 02:57:37 [notice] 2866#2866: greet create_srv_conf in /work/nginx.conf:31
2026/11/07 02:57:37 [notice] 2866#2866: greet create_loc_conf in /work/nginx.conf:31
2026/11/07 02:57:37 [notice] 2866#2866: greet create_loc_conf in /work/nginx.conf:35
2026/11/07 02:57:37 [notice] 2866#2866: greet merge_srv_conf: line 8 -> line 12, tag set here in /work/nginx.conf:39
2026/11/07 02:57:37 [notice] 2866#2866: greet merge_loc_conf: line 8 -> line 12, message inherited, shout inherited in /work/nginx.conf:39
2026/11/07 02:57:37 [notice] 2866#2866: greet merge_loc_conf: line 12 -> line 16, message inherited, shout inherited in /work/nginx.conf:39
2026/11/07 02:57:37 [notice] 2866#2866: greet merge_loc_conf: line 12 -> line 20, message set here, shout set here in /work/nginx.conf:39
2026/11/07 02:57:37 [notice] 2866#2866: greet merge_loc_conf: line 20 -> line 25, message inherited, shout inherited in /work/nginx.conf:39
2026/11/07 02:57:37 [notice] 2866#2866: greet merge_srv_conf: line 8 -> line 31, tag inherited in /work/nginx.conf:39
2026/11/07 02:57:37 [notice] 2866#2866: greet merge_loc_conf: line 8 -> line 31, message set here, shout inherited in /work/nginx.conf:39
2026/11/07 02:57:37 [notice] 2866#2866: greet merge_loc_conf: line 31 -> line 35, message inherited, shout inherited in /work/nginx.conf:39
2026/11/07 02:57:37 [notice] 2866#2866: greet postconfiguration: index=89 ctx_index=57 modules_n=90 http modules=58 ngx_http_max_module=58 in /work/nginx.conf:39
nginx: the configuration file /work/nginx.conf syntax is ok
nginx: configuration file /work/nginx.conf test is successful
The timestamp and pid (2866) differ on every run.
- Creation happens the moment a block opens. One srv and one loc at line 8 (
http {), one srv and one loc at lines 12 and 31 (server {), one loc at lines 16, 20, 25 and 35 (location {) — exactly one per block. A location inside a location, like/loud/inner/on line 25, gets one too. - All merging happens at line 39, that is, after the closing brace of
http {}has been read.ngx_conf_parse()reads the whole block and returns, and only then doesngx_http_block()start merging (section 6). So inside a merge function, everything written in the lower blocks is already known. - postconfiguration comes after merging. The greet module's
indexis 89 — the first free number after static modules 0–88 — itsctx_indexis 57, there are 58 HTTP modules (57 static + greet), andngx_http_max_moduleis 58 too.
Line numbers are from the example nginx.conf. All eight HTTP module callbacks are called inside this picture, once each, while the config is read.
postconfiguration comes late for a reason. Many modules use it to register phase handlers (for example, ngx_http_access_module adds a function to the ACCESS phase array) or to hook themselves into the filter chain. That requires the phase arrays to exist (ngx_http_init_phases(), ngx_http.c line 294), so it runs after them. ngx_http_init_phase_handlers() (line 329), which flattens the registered handlers into a single array, runs later still. Phases are in part 9 and the filter chain in part 10.
Checking with gdb: nested blocks mean nested ngx_conf_parse() calls
The create_loc_conf for /loud/inner/ is the fifth call. The function lives in a dynamic module that isn't loaded yet when gdb starts, so I enabled set breakpoint pending on, stopped at the fifth call and printed the stack:
gdb -batch -q \
-ex 'set breakpoint pending on' \
-ex 'break ngx_http_greet_create_loc_conf' \
-ex 'run' -ex 'continue 4' \
-ex 'bt' \
-ex 'frame 1' \
-ex 'print ctx->main_conf == pctx->main_conf' \
-ex 'print ctx->srv_conf == pctx->srv_conf' \
-ex 'print ctx->loc_conf == pctx->loc_conf' \
-ex 'print ngx_http_max_module' \
-ex 'kill' \
--args nginx -t -q -c /work/nginx.conf 2>&1 \
grep -v '^\[Thread\|^Using host libthread\|^warning: Error disabling\|^Breakpoint 1 at\|^Make breakpoint pending\|^\[Inferior'
#0 ngx_http_greet_create_loc_conf (cf=0x7fffffffe7f0) at /work/ngx_http_greet_module.c:219
#1 0x00005555555f19c2 in ngx_http_core_location (cf=0x7fffffffe7f0, cmd=0x555555728430 <ngx_http_core_commands+784>, dummy=0x555555799388) at src/http/ngx_http_core_module.c:3197
#2 0x00005555555a4359 in ngx_conf_handler (cf=0x7fffffffe7f0, last=1) at src/core/ngx_conf_file.c:469
#3 0x00005555555a3e89 in ngx_conf_parse (cf=0x7fffffffe7f0, filename=0x0) at src/core/ngx_conf_file.c:325
#4 0x00005555555f20f9 in ngx_http_core_location (cf=0x7fffffffe7f0, cmd=0x555555728430 <ngx_http_core_commands+784>, dummy=0x555555799388) at src/http/ngx_http_core_module.c:3360
#5 0x00005555555a4359 in ngx_conf_handler (cf=0x7fffffffe7f0, last=1) at src/core/ngx_conf_file.c:469
#6 0x00005555555a3e89 in ngx_conf_parse (cf=0x7fffffffe7f0, filename=0x0) at src/core/ngx_conf_file.c:325
#7 0x00005555555f161a in ngx_http_core_server (cf=0x7fffffffe7f0, cmd=0x555555728200 <ngx_http_core_commands+224>, dummy=0x555555786ef0) at src/http/ngx_http_core_module.c:3102
#8 0x00005555555a4359 in ngx_conf_handler (cf=0x7fffffffe7f0, last=1) at src/core/ngx_conf_file.c:469
#9 0x00005555555a3e89 in ngx_conf_parse (cf=0x7fffffffe7f0, filename=0x0) at src/core/ngx_conf_file.c:325
#10 0x00005555555e73ff in ngx_http_block (cf=0x7fffffffe7f0, cmd=0x5555557278a0 <ngx_http_commands>, conf=0x555555785350) at src/http/ngx_http.c:240
#11 0x00005555555a4359 in ngx_conf_handler (cf=0x7fffffffe7f0, last=1) at src/core/ngx_conf_file.c:469
#12 0x00005555555a3e89 in ngx_conf_parse (cf=0x7fffffffe7f0, filename=0x555555784338) at src/core/ngx_conf_file.c:325
#13 0x000055555559fb97 in ngx_init_cycle (old_cycle=0x7fffffffe930) at src/core/ngx_cycle.c:286
#14 0x000055555557ee55 in main (argc=5, argv=0x7fffffffed08) at src/core/nginx.c:300
#1 0x00005555555f19c2 in ngx_http_core_location (cf=0x7fffffffe7f0, cmd=0x555555728430 <ngx_http_core_commands+784>, dummy=0x555555799388) at src/http/ngx_http_core_module.c:3197
3197 module->create_loc_conf(cf);
$1 = 1
$2 = 1
$3 = 0
$4 = 58
Addresses may differ between runs. I trimmed the breakpoint notices at the top (Breakpoint 1, … and so on) and the Kill the program … line at the end.
- The three frames
ngx_conf_parse→ngx_conf_handler→ block handler repeat once per nesting level: the file (#12, with afilename) →http(#10) →server(#7) →location /loud/(#4) →location /loud/inner/(#1). The config parser descends through blocks by recursion, and each block handler adjustscfon the way. Every frame'scfhas the same address (0x7fffffffe7f0): one local variable,confinngx_init_cycle(), is shared all the way down, with fields changed and restored. - In frame #1 (
ngx_http_core_location), comparing the new contextctxwith the parent contextpctxshowsmain_confandsrv_confare the same arrays (1) and onlyloc_confis a different array (0) — just like the diagram above.
Easy to trip on: log lines written while the config is read don't go to the config's error_log
At first I ran only nginx -t -c /work/nginx.conf, and greet's notice lines showed up neither on screen nor in /tmp/u05/error.log. While the config is read, cf->log is the old cycle's log that ngx_init_cycle() received (ngx_cycle.c lines 67 and 272). On first start that is the default log opened by ngx_log_init() — logs/error.log as fixed at build time unless -e is given, at level NGX_LOG_NOTICE (around lines 324–334 of ngx_log.c). The error_log /tmp/u05/error.log in the config isn't open yet. So the notices went to /opt/nginx/logs/error.log. [emerg] errors do show on screen because WARN and more severe levels are also written to stderr, separately from the file (the condition around lines 198–199 of ngx_log.c). Pointing the default log at the screen with -e stderr gives the output above.
6. Merging — once at config time, only a pointer at request time
Where merging happens: ngx_http_merge_servers()
Merging starts after ngx_http_block() has read the whole block:
cmcf = ctx->main_conf[ngx_http_core_module.ctx_index];
cscfp = cmcf->servers.elts;
for (m = 0; cf->cycle->modules[m]; m++) {
if (cf->cycle->modules[m]->type != NGX_HTTP_MODULE) {
continue;
}
module = cf->cycle->modules[m]->ctx;
mi = cf->cycle->modules[m]->ctx_index;
/* init http{} main_conf's */
if (module->init_main_conf) {
rv = module->init_main_conf(cf, ctx->main_conf[mi]);
if (rv != NGX_CONF_OK) {
goto failed;
}
}
rv = ngx_http_merge_servers(cf, cmcf, module, mi);
if (rv != NGX_CONF_OK) {
goto failed;
}
}
src/http/ngx_http.c lines 251–275
The outer loop is over modules. It picks one module, merges that module's config across every server and location to the bottom, then moves to the next module. The server list cmcf->servers is the array ngx_http_core_server() appends to each time it meets a server block (ngx_http_core_module.c lines 3086–3093).
cscfp = cmcf->servers.elts;
ctx = (ngx_http_conf_ctx_t *) cf->ctx;
saved = *ctx;
rv = NGX_CONF_OK;
for (s = 0; s < cmcf->servers.nelts; s++) {
/* merge the server{}s' srv_conf's */
ctx->srv_conf = cscfp[s]->ctx->srv_conf;
if (module->merge_srv_conf) {
rv = module->merge_srv_conf(cf, saved.srv_conf[ctx_index],
cscfp[s]->ctx->srv_conf[ctx_index]);
if (rv != NGX_CONF_OK) {
goto failed;
}
}
if (module->merge_loc_conf) {
/* merge the server{}'s loc_conf */
ctx->loc_conf = cscfp[s]->ctx->loc_conf;
rv = module->merge_loc_conf(cf, saved.loc_conf[ctx_index],
cscfp[s]->ctx->loc_conf[ctx_index]);
if (rv != NGX_CONF_OK) {
goto failed;
}
/* merge the locations{}' loc_conf's */
clcf = cscfp[s]->ctx->loc_conf[ngx_http_core_module.ctx_index];
rv = ngx_http_merge_locations(cf, clcf->locations,
cscfp[s]->ctx->loc_conf,
module, ctx_index);
if (rv != NGX_CONF_OK) {
goto failed;
}
}
}
failed:
*ctx = saved;
return rv;
src/http/ngx_http.c lines 573–621
Three steps per server:
- http-level srv_conf (parent) → this server's srv_conf (child):
merge_srv_conf - http-level loc_conf (parent) → this server-level loc_conf (child):
merge_loc_conf - this server-level loc_conf (parent) → the locations inside it:
ngx_http_merge_locations()walks the location list callingmerge_loc_conf, and calls itself again for locations nested inside a location (the recursion on lines 626–667).
The merge order in example 2's log — 8 -> 12 (srv), 8 -> 12 (loc), 12 -> 16, 12 -> 20, 20 -> 25, then the second server — is exactly this order. Because parents are merged first, by the time 20 -> 25 runs, the line-20 struct (/loud/) has already inherited everything from above. So merging one level at a time is enough to carry a top-level value all the way down.
The circled numbers are the order of the merge log. "Set here / inherited" in each box comes from the merge log, and the final values from the curl output below.
During merging, ctx->srv_conf and ctx->loc_conf are also switched to the server or location being merged (lines 582, 596 and 649). That way a merge function can use ngx_http_conf_get_module_*_conf(cf, …) to look at another module's config at the same level.
The shape of a merge function: "if not written, the parent's; if the parent didn't write it either, the default"
Most modules' merge functions are a handful of macro lines:
#define ngx_conf_merge_value(conf, prev, default) \
if (conf == NGX_CONF_UNSET) { \
conf = (prev == NGX_CONF_UNSET) ? default : prev; \
}
src/core/ngx_conf_file.h lines 209–212
This is why section 3 insisted that create_*_conf initialize fields to NGX_CONF_UNSET: you have to recognize "not written" to decide whether to take the parent's value. Strings use ngx_conf_merge_str_value, which tests data == NULL (lines 244–253), and there are matching macros for sizes, times, buffer counts and so on.
ngx_http_core_module's merge function, ngx_http_core_merge_loc_conf(), starts on line 3814 and runs to line 4103, close to 300 lines, mostly lines like this:
ngx_conf_merge_value(conf->sendfile, prev->sendfile, 0);
src/http/ngx_http_core_module.c line 3956
The default is the third argument of this line. That's how to confirm a default from the source without looking it up in the docs. A few samples:
| Directive | Default in the merge line | Location | Value written in the source's conf/nginx.conf |
|---|---|---|---|
sendfile |
0 (off) |
ngx_http_core_module.c line 3956 | sendfile on; (line 27) |
keepalive_timeout |
75000 ms (75 s) |
lines 3988–3989 | keepalive_timeout 65; (line 31) |
root |
"html" (path relative to the prefix) |
lines 3823–3837 | root html; inside a location |
sendfile_max_chunk |
2 * 1024 * 1024 (2 MB) |
lines 3957–3958 | not written |
sendfile is off by default, yet "nginx uses sendfile" is common knowledge because the sample config file shipped with nginx says sendfile on;. Write a config from scratch and it runs with sendfile off. Likewise the 65 seconds of keepalive_timeout in the sample is a written value; delete it and you get 75 seconds. root uses hand-written code instead of a simple macro — alias and a path containing variables (root_lengths, root_values) must be inherited as a set, and if the parent has nothing either, "html" is turned into a full path relative to the prefix (lines 3823–3837).
Example 2 continued: the values each request got
I started nginx with the same config and sent a request to each location:
=== 5. curl: merged values per location ===
http://127.0.0.1:8080/ tag=A (srv_conf from line 12), message="from http" shout=off (loc_conf from line 16)
http://127.0.0.1:8080/loud/ TAG=A (SRV_CONF FROM LINE 12), MESSAGE="FROM LOCATION" SHOUT=ON (LOC_CONF FROM LINE 20)
http://127.0.0.1:8080/loud/inner/ TAG=A (SRV_CONF FROM LINE 12), MESSAGE="FROM LOCATION" SHOUT=ON (LOC_CONF FROM LINE 25)
http://127.0.0.1:8081/ tag=- (srv_conf from line 31), message="from server 2" shout=off (loc_conf from line 35)
/(line 16) wrote nothing, yet got"from http"from line 10. It came down two levels: line 8 (http) → line 12 (server A level) → line 16./loud/inner/(line 25) wrote nothing either, yet got"from location"andshout onwritten in/loud/. A location inside a location merges with the outer location as its parent./on 8081 got line 33 — thegreet_messagewritten in the server block. As explained above, that value sat in the server level's loc_conf and came down to the location on line 35.greet_tagis in neither http nor this server, so it has the default"-".
Where shout is on, the module prints the whole line in upper case (that's how I wrote the example module).
No merging at request time
Once nginx decides which location a request goes to, it just attaches that location's already merged array to the request. When the request struct is created it attaches the default server's arrays (ngx_http_request.c lines 601–603); after the location is found it switches to that location's:
if (len == (size_t) node->len) {
if (node->exact) {
r->loc_conf = node->exact->loc_conf;
return NGX_OK;
} else {
r->loc_conf = node->inclusive->loc_conf;
return NGX_AGAIN;
}
}
src/http/ngx_http_core_module.c lines 1598–1608
node->exact->loc_conf is the array saved by clcf->loc_conf = ctx->loc_conf; in section 5. Changing one pointer switches every module's location config at once. After that a module reads its own values with ngx_http_get_module_loc_conf(r, module) — r->loc_conf[module.ctx_index] — as the example's ngx_http_greet_handler() does. The location lookup tree and this function itself are covered in detail in part 9.
A request only switches a pointer to an already-merged array. /loud/ is the location from Example 2, used as an example.
Easy to trip on: httpd merges at request time too
httpd, as we saw in httpd part 7, merges server configs at startup but merges directory configs (<Directory>, <Location>, .htaccess) on every request with ap_merge_per_dir_configs(). Which sections apply depends on the request path, several can overlap, and .htaccess is read at request time. nginx picks exactly one location per request (they don't stack), and nesting comes only from the block structure of the config file, so every possible merge result can be built ahead of time at startup. The flip side is that nginx has no "config read at request time" like .htaccess. For the same reason, changing a directive value requires a reload that builds a new cycle and redoes all the merging from scratch (part 6).
Going deeper
- Block handling in events, stream and mail:
ngx_events_block()(src/event/ngx_event.c,ngx_count_modulesaround line 971),ngx_stream_block()(src/stream/ngx_stream.c, around line 117),ngx_mail_block()(src/mail/ngx_mail.c, around line 94). Same shape asngx_http_block(). handler/handler_confinngx_conf_t— for blocks liketypes {}, where one function receives every line instead of a directive table: ngx_conf_file.c lines 298–322- The other slot functions (
ngx_conf_set_size_slot,_msec_slot,_enum_slot,_bitmask_slot,_keyval_slot…) andpostchecks (ngx_conf_check_num_bounds,ngx_conf_deprecated):src/core/ngx_conf_file.cfrom line 1098 includeand config given with-g:ngx_conf_include(),ngx_conf_param()— part 4- Directive values containing variables (
ngx_http_complex_value_t,ngx_http_set_complex_value_slot):src/http/ngx_http_script.c— the script engine is in part 9 - Building the location tree,
ngx_http_init_locations()andngx_http_init_static_location_trees(): from ngx_http.c line 670 — part 9 - The
upstream {}block and its context bitNGX_HTTP_UPS_CONF:ngx_http_upstream()(src/http/ngx_http_upstream.c) — part 11
Summary
| Concept | One-line summary |
|---|---|
| Module | One ngx_module_t struct variable. type gives its kind, ctx points to type-specific callbacks, commands to the directive table |
objs/ngx_modules.c |
The array of module addresses configure writes. Its order is the order of directive lookup and callback calls. 89 static modules in this build |
index / ctx_index |
Number among all modules (cycle->conf_ctx slot) / number among modules of one type (main_conf[], srv_conf[], loc_conf[] slot) |
load_module |
Inserts the ngx_modules[] of a dlopened .so with ngx_add_module(). Version and signature must match, and it must come before the blocks (modules_used) |
ngx_command_t |
Name, context and argument bits (type), handler (set), array selector (conf), field selector (offset) |
| Slot functions | ngx_conf_set_flag_slot and friends. Write to conf + offset without knowing the struct. "is duplicate" if the field isn't UNSET |
ngx_conf_handler() |
Compares against every module's directives in turn. Module type → context bits → argument count → conf pointer → set |
ngx_http_conf_ctx_t |
Three pointer arrays: main_conf, srv_conf, loc_conf. One per block. Lower blocks share the upper arrays and create only their own level |
| Callback order | create when a block opens → after everything up to } is read, init_main_conf and merge → postconfiguration |
| Merging | At config time, per module, one level at a time: server → location → nested location. The default is the third argument of ngx_conf_merge_* |
| At request time | No merging. The found location's clcf->loc_conf array is just attached to r->loc_conf |
What's next
With the config read, nginx becomes a master process and starts the workers. We'll see where init_process — passed over here as a "process lifetime callback" — is called, and how nginx -s reload redoes everything in this post up to the merge from scratch in a new cycle and then quietly retires the old workers. That's part 6: Master and Workers — Processes, Signals and Zero-Downtime Reload.
The httpd series' part 6: Modules and Hooks and part 7: The Config File and Directives are a different answer to the same problem. A module being a single struct, a directive table (command_rec ↔ ngx_command_t), and per-context config create and merge callbacks are strikingly similar. Two things differ. httpd modules join request processing by hanging functions on hooks, while nginx modules plug directly into phase arrays and the filter chain in postconfiguration (parts 9 and 10). And httpd merges directory configs at request time, while nginx merges everything ahead of time at startup.
- Part 4: Startup and Reading the Config — From main() to ngx_init_cycle
- Part 9: The 11 Phases and Finding the location — In What Order a Request Is Handled
- Part 10: Filters and Static Files — How a Response Flows Out
- httpd part 2: Before Reading a Server Written in C — Structs, Function Pointers, Macros