What this post covers
This is the ninth post in the "Digging into nginx's Source" series. In Part 8 we read the request line and headers off a connection, built an ngx_http_request_t, and picked a server {} block from the Host header. The last function in Part 8, ngx_http_process_request(), ends by calling ngx_http_handler(r). This post starts right there.
There are four questions to answer.
- Which steps (phases) does a request go through, in what order, and how do modules plug into them?
- What do
locationblocks look like in memory, and who wins when=,^~,~and plain prefixes overlap? - What is the difference between
rewrite ... lastandbreakin the source, and where does processing go back to whentry_files,error_pageorindex"handle it again with a different URI"? - How are variables such as
$uriand the regexes ofrewriteevaluated for each request?
| Item | Details |
|---|---|
| Target | nginx |
| Version | nginx 1.31.9 (master, development version) |
| Commit | 9a7439fd79 (2026-10-23) |
| Files read most | src/http/ngx_http_core_module.c/.h, src/http/ngx_http.c, src/http/modules/ngx_http_rewrite_module.c, src/http/ngx_http_script.c, src/http/ngx_http_variables.c |
Every line number in this post refers to that commit. Part 1 explains how to get the source at the same commit and build the Docker image used for the examples.
On the map from Part 2, this post zooms into the Phases · location box inside "C. One HTTP request". ngx_http_core_module.c alone is over 5,500 lines, so this post keeps only the main thread that a first-time reader can follow. What was left out is named, with its location, under "What this post leaves out" at the end.
Where this post sits on the Part 2 map: after the request has been read (Part 8) and before the response is produced and sent (Parts 10 and 11).
Words to know first
A phase is one of the eleven slots that nginx divides request processing into — "the slot that rewrites the address", "the slot that picks the location", "the slot that checks access", "the slot that produces the response", and so on. The nginx core fixes the order; modules register functions saying "call me in this slot".
A phase handler is a function a module registered for one phase. They all have the same shape, ngx_int_t handler(ngx_http_request_t *r), and the return value says "handled" (NGX_OK), "not my job" (NGX_DECLINED), "not finished yet, call me later" (NGX_AGAIN), or an HTTP status code (403 and so on).
A checker is a core function, one per phase, that calls the handler on the core's behalf and interprets its return value. The same NGX_OK can mean "skip to the next phase" in one phase and "finish the response" in another. It is function pointers two layers deep (if C function pointers are new to you, httpd Part 2 is a good warm-up).
loc_conf is an array of pointers holding "every module's configuration for one location". The slot number is the module's number (ctx_index). The request has its own r->loc_conf too, and picking a location means swapping this one pointer for the chosen location's array. A module gets its own settings with ngx_http_get_module_loc_conf(r, module), which is (r)->loc_conf[module.ctx_index] (ngx_http_config.h line 58). How the array is built is covered in Part 5.
A content handler is the function that produces the response body. Directives that take over a whole location, like proxy_pass, fastcgi_pass and stub_status, don't register in a phase; they plant a handler in that location's configuration instead.
An internal redirect means changing the URI in the middle of processing and processing it again. The browser never knows. A named location is a location like location @name {} that cannot be found by URI and is used only as the target of an internal redirect.
A variable is a name like $uri or $arg_beta whose value differs per request. When the config is read, the name is turned into a number (an index); the value is computed the first time a request asks for it (section 5).
1. The 11 phases — the core sets the order, modules fill them in
The phases are defined by a single enum.
typedef enum {
NGX_HTTP_POST_READ_PHASE = 0,
NGX_HTTP_SERVER_REWRITE_PHASE,
NGX_HTTP_FIND_CONFIG_PHASE,
NGX_HTTP_REWRITE_PHASE,
NGX_HTTP_POST_REWRITE_PHASE,
NGX_HTTP_PREACCESS_PHASE,
NGX_HTTP_ACCESS_PHASE,
NGX_HTTP_POST_ACCESS_PHASE,
NGX_HTTP_PRECONTENT_PHASE,
NGX_HTTP_CONTENT_PHASE,
NGX_HTTP_LOG_PHASE
} ngx_http_phases;
src/http/ngx_http_core_module.h lines 110–129
That is eleven, numbered 0 to 10. The blank lines are a hint: FIND_CONFIG, REWRITE and POST_REWRITE form one group, and ACCESS and POST_ACCESS another. To find out who goes into each phase, search the source for places that ngx_array_push onto cmcf->phases[...].handlers.
Blue boxes are phases where modules register handlers; gray boxes are phases where only the core works. The green CONTENT box is where the response is made, and LOG is gray because, although access_log registers there, it is called separately, outside the ph[] array below. The module names are for this post's build (the configure arguments from Part 1).
| phase | What you know from nginx.conf | Where it is registered (base commit) |
|---|---|---|
| 0 POST_READ | real_ip_header |
ngx_http_realip_module.c line 526 |
| 1 SERVER_REWRITE | rewrite, return, set, if directly under server {} |
ngx_http_rewrite_module.c line 279 |
| 2 FIND_CONFIG | picking the location |
(core) |
| 3 REWRITE | rewrite, return, set, if inside location {} |
ngx_http_rewrite_module.c line 286 |
| 4 POST_REWRITE | finding the location again after rewrite ... last |
(core) |
| 5 PREACCESS | limit_req, limit_conn |
ngx_http_limit_req_module.c line 1094, etc. |
| 6 ACCESS | allow/deny, auth_basic, auth_request |
ngx_http_access_module.c line 455, etc. |
| 7 POST_ACCESS | settling the result of satisfy any |
(core) |
| 8 PRECONTENT | try_files, mirror |
ngx_http_try_files_module.c line 411, etc. |
| 9 CONTENT | proxy_pass, static files, index, autoindex |
ngx_http_static_module.c line 289, etc. |
| 10 LOG | access_log |
ngx_http_log_module.c line 2016 |
The code a module uses to plug in is tiny. The rewrite module's postconfiguration callback puts the same function into two phases; here is the first half.
h = ngx_array_push(&cmcf->phases[NGX_HTTP_SERVER_REWRITE_PHASE].handlers);
if (h == NULL) {
return NGX_ERROR;
}
*h = ngx_http_rewrite_handler;
src/http/modules/ngx_http_rewrite_module.c lines 279–284
cmcf is the core module's main configuration, and its phases[NGX_HTTP_LOG_PHASE + 1] (line 178) holds one ngx_http_phase_t per phase, whose single field handlers is the ngx_array_t that collects the handlers (the dynamic array from Part 3; lines 150–152). That is the whole registration. No macros, no ordering argument.
Comparing with httpd from httpd Part 10 makes the difference clear. httpd has a named hook per step, each hook has a rule such as "the first one to answer wins" (RUN_FIRST) or "run them all" (RUN_ALL) baked in by macros, and modules choose their order with values like APR_HOOK_FIRST. In nginx every handler has the same shape, the per-phase rules live in the checker, and modules cannot choose their order. The order within a phase comes from the module list that configure generates (objs/ngx_modules.c, Part 5). And while httpd's configuration-choosing map_to_storage was a hook, nginx's FIND_CONFIG is not open to modules.
2. Once, at config time — flattening phases[] into a single ph[] array
Instead of looking through eleven separate arrays for every request, nginx flattens them into one array after reading the http block. One slot is an ngx_http_phase_handler_t.
typedef struct ngx_http_phase_handler_s ngx_http_phase_handler_t;
typedef ngx_int_t (*ngx_http_phase_handler_pt)(ngx_http_request_t *r,
ngx_http_phase_handler_t *ph);
struct ngx_http_phase_handler_s {
ngx_http_phase_handler_pt checker;
ngx_http_handler_pt handler;
ngx_uint_t next;
};
typedef struct {
ngx_http_phase_handler_t *handlers;
ngx_uint_t server_rewrite_index;
ngx_uint_t location_rewrite_index;
} ngx_http_phase_engine_t;
src/http/ngx_http_core_module.h lines 131–147
| Field | Meaning |
|---|---|
checker |
the core function that handles this slot. All slots of one phase share a checker |
handler |
the handler a module registered. NULL for core-only slots |
next |
the slot to jump to when the answer is "skip this phase". Usually the first slot of the next phase |
ngx_http_init_phase_handlers(), which builds the array, is called after ngx_http_block() has run every module's postconfiguration (ngx_http.c lines 303–331). It first adds up the core slots and the module handlers and allocates (lines 470–479), and two things stand out. The counting loop only runs up to i < NGX_HTTP_LOG_PHASE, so the LOG phase is not in this array (section 3); and the extra sizeof(void *) that ngx_pcalloc() zero-fills leaves a final slot with checker == NULL — the end marker. Then it walks the phases and fills the slots.
case NGX_HTTP_FIND_CONFIG_PHASE:
find_config_index = n;
ph->checker = ngx_http_core_find_config_phase;
n++;
ph++;
continue;
/* ... */
case NGX_HTTP_POST_REWRITE_PHASE:
if (use_rewrite) {
ph->checker = ngx_http_core_post_rewrite_phase;
ph->next = find_config_index;
n++;
ph++;
}
continue;
case NGX_HTTP_ACCESS_PHASE:
checker = ngx_http_core_access_phase;
n++;
break;
/* ... */
default:
checker = ngx_http_core_generic_phase;
}
n += cmcf->phases[i].handlers.nelts;
for (j = cmcf->phases[i].handlers.nelts - 1; j >= 0; j--) {
ph->checker = checker;
ph->handler = h[j];
ph->next = n;
ph++;
}
src/http/ngx_http.c lines 500–556
- Inserting core slots:
FIND_CONFIG,POST_REWRITEandPOST_ACCESSadd a slot with a checker but no handler andcontinue.POST_REWRITE'snext = find_config_index— "if the URI changed, go find the location again" is that one line. - Choosing the checker: both rewrite phases get
ngx_http_core_rewrite_phase,ACCESSgets the access checker,CONTENTthe content checker (in the elided part), and the rest getngx_http_core_generic_phase. nextis the first slot of the next phase: the phase's handler count is added tonfirst, then stored. The extran++forACCESSmakes it skip thePOST_ACCESSslot that follows (thesatisfy anycase in section 3).- Copied backwards: the inner
forcounts down fromnelts - 1to 0, so handlers registered later are called first.
Example 1: printing the ph[] array with gdb
I printed the array of a running nginx. This example was run with nginx 1.31.9 built unchanged from the base commit (9a7439fd79) in a Docker container (Debian bookworm, --with-debug). The example folder is ex/u09-phase-engine; this is the http {} part of its config (the rest, such as error_log ... debug and events {}, is in the folder's nginx.conf).
http {
access_log /tmp/u09/access.log;
root /tmp/u09/www;
server {
listen 8080;
location / {
}
location /admin/ {
deny all;
}
}
}
To make gdb's job easy, nginx runs as a single process with -g 'daemon off; master_process off;', a breakpoint goes on ngx_http_core_run_phases, and curl requests /index.html. When it stops, a gdb script (phases.gdb) takes the core module's main configuration out of r->main_conf and prints phase_engine.handlers slot by slot with info symbol until it reaches the slot whose checker is NULL. run.sh tidies that output (dropping in section .text and the binary's path) and puts each slot's checker <- handler on one line.
=== 1. ph[] array (gdb, master_process off) ===
server_rewrite_index=1 location_rewrite_index=3
ph[ 0] next= 1 ngx_http_core_generic_phase <- ngx_http_realip_handler
ph[ 1] next= 2 ngx_http_core_rewrite_phase <- ngx_http_rewrite_handler
ph[ 2] next= 0 ngx_http_core_find_config_phase
ph[ 3] next= 4 ngx_http_core_rewrite_phase <- ngx_http_rewrite_handler
ph[ 4] next= 2 ngx_http_core_post_rewrite_phase
ph[ 5] next= 8 ngx_http_core_generic_phase <- ngx_http_realip_handler
ph[ 6] next= 8 ngx_http_core_generic_phase <- ngx_http_limit_req_handler
ph[ 7] next= 8 ngx_http_core_generic_phase <- ngx_http_limit_conn_handler
ph[ 8] next=12 ngx_http_core_access_phase <- ngx_http_access_handler
ph[ 9] next=12 ngx_http_core_access_phase <- ngx_http_auth_basic_handler
ph[10] next=12 ngx_http_core_access_phase <- ngx_http_auth_request_handler
ph[11] next=12 ngx_http_core_post_access_phase
ph[12] next=14 ngx_http_core_generic_phase <- ngx_http_try_files_handler
ph[13] next=14 ngx_http_core_generic_phase <- ngx_http_mirror_handler
ph[14] next=18 ngx_http_core_content_phase <- ngx_http_index_handler
ph[15] next=18 ngx_http_core_content_phase <- ngx_http_autoindex_handler
ph[16] next=18 ngx_http_core_content_phase <- ngx_http_gzip_static_handler
ph[17] next=18 ngx_http_core_content_phase <- ngx_http_static_handler
ph[18] checker=NULL (end)
Example 1's output as a picture. The slot numbers come from this build's set of modules and change if modules are added or removed.
server_rewrite_index=1andlocation_rewrite_index=3are the first slots of the two rewrite phases. In section 6, internal redirects and named locations come back to these numbers.- realip appears twice (ph[0] and ph[5]). The realip module registers the same function in both
POST_READandPREACCESS(ngx_http_realip_module.clines 526–533). - The reverse order is visible. In this build's
ngx_modules.cthe module order isstatic(line 113) →gzip_static→autoindex→index(line 116), andauth_request(line 119) →auth_basic→access(line 121), yet in the arrayindexcomes beforestaticandaccessbeforeauth_request.indexhas to go first so it can turn a request ending in/intoindex.html, andstatictakes whatever is left at the end. - There are slots for
limit_req,auth_basicandtry_fileseven though the config uses none of them. There is one array for the whole http block, and every compiled-in module registers. In a location without those settings, the handler simply returnsNGX_DECLINED.
3. Every time, per request — ngx_http_core_run_phases() and the checkers
The starting point is ngx_http_handler(), called at the very end of Part 8's ngx_http_process_request() (ngx_http_request.c line 2217). After some housekeeping such as keep-alive, it sets r->phase_handler, hooks up r->write_event_handler = ngx_http_core_run_phases, and calls that function straight away (lines 854–890). r->phase_handler is an integer that remembers "which slot of ph[] are we in"; a new request starts at 0 and a request that came back through an internal redirect (r->internal) starts at server_rewrite_index (section 6). write_event_handler is for waking up after a pause. That run loop is the shortest and most important function in this post.
void
ngx_http_core_run_phases(ngx_http_request_t *r)
{
ngx_int_t rc;
ngx_http_phase_handler_t *ph;
ngx_http_core_main_conf_t *cmcf;
cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);
ph = cmcf->phase_engine.handlers;
while (ph[r->phase_handler].checker) {
rc = ph[r->phase_handler].checker(r, &ph[r->phase_handler]);
if (rc == NGX_OK) {
return;
}
}
}
src/http/ngx_http_core_module.c lines 893–912
The loop never advances phase_handler itself. The checker moves r->phase_handler, and the loop only looks at what the checker returns. Here the return values mean something different from a handler's: a checker's NGX_AGAIN means "keep going" and NGX_OK means "that's all for now". The loop exits with NGX_OK either when the response is finished or when a handler has to wait for something (the request body, a subrequest, a limit_req delay …). In the second case r->phase_handler is left as it is, so when write_event_handler brings us back later, processing resumes at the slot where it stopped. That is how nginx steps through phases on top of the event loop from Part 7 without blocking.
Drawn for the generic and access checkers. Each checker reads the same return value differently (table below).
Seven checkers — same return values, different readings
Here is the basic one, ngx_http_core_generic_phase().
rc = ph->handler(r);
if (rc == NGX_OK) {
r->phase_handler = ph->next;
return NGX_AGAIN;
}
if (rc == NGX_DECLINED) {
r->phase_handler++;
return NGX_AGAIN;
}
if (rc == NGX_AGAIN || rc == NGX_DONE) {
return NGX_OK;
}
/* rc == NGX_ERROR || rc == NGX_HTTP_... */
ngx_http_finalize_request(r, rc);
return NGX_OK;
}
src/http/ngx_http_core_module.c lines 928–949
If the handler returns NGX_OK, the rest of this phase's handlers are skipped and we go to next; on NGX_DECLINED we go to the next handler of the same phase. NGX_AGAIN/NGX_DONE stop the loop, and anything else finishes the request with that value. The other checkers vary this pattern a little.
| checker | used by | differs from generic | location |
|---|---|---|---|
ngx_http_core_generic_phase |
POST_READ, PREACCESS, PRECONTENT | (baseline) | lines 915–949 |
ngx_http_core_rewrite_phase |
SERVER_REWRITE, REWRITE | only DECLINED moves on. NGX_OK and NGX_AGAIN are also values that finish the request |
lines 952–976 |
ngx_http_core_find_config_phase |
FIND_CONFIG | no handler. Finds the location and swaps r->loc_conf |
lines 979–1071 |
ngx_http_core_post_rewrite_phase |
POST_REWRITE | goes back to FIND_CONFIG if r->uri_changed |
lines 1074–1115 |
ngx_http_core_access_phase |
ACCESS | skips everything for subrequests. Reads OK and 403 differently depending on satisfy |
lines 1118–1192 |
ngx_http_core_post_access_phase |
POST_ACCESS | finishes with the noted r->access_code, if any |
lines 1195–1226 |
ngx_http_core_content_phase |
CONTENT | only the content handler, if there is one. 403 or 404 if nobody takes the request | lines 1301–1351 |
The rewrite checker reads NGX_OK as "done" because of how the rewrite module works. A return with a body, like return 200 "...";, sends the response on the spot with ngx_http_send_response() and returns its result (usually NGX_OK); a return 404; without a body returns the status code (ngx_http_script.c lines 1577–1585). If nothing happened, it is NGX_DECLINED (section 5). Either way there is no "handled, move on to the next phase" case, and NGX_OK means "the response has already been sent, wrap it up".
The find_config checker calls section 4's ngx_http_core_find_location(), checks the chosen location's internal flag (404 for an outside request) and writes using configuration "..." to the debug log (lines 998–1008). The ngx_http_update_location_config() it calls next copies the chosen location's settings (error log, sendfile, keep-alive limits …) onto the request and connection, and its last lines set the content handler.
if (clcf->handler) {
r->content_handler = clcf->handler;
}
src/http/ngx_http_core_module.c lines 1431–1433
The clcf->handler = ngx_http_proxy_handler; planted when the proxy_pass directive was read (ngx_http_proxy_module.c line 4397) becomes the request's content handler here. If it is set, the content checker calls only that at its very start (lines 1309–1313) and finishes, so index and static are never called in that location. The story continues in Part 11.
satisfy any — why ACCESS's next skips POST_ACCESS
With satisfy all (the default) a single refusal from any access module ends it; with satisfy any a single approval lets the request through. The access checker reads return values differently depending on clcf->satisfy (lines 1146–1181). Under satisfy any, NGX_OK means "no need to look at the remaining checks", so it jumps to ph->next. That is why section 2 pointed ACCESS's next at the slot after POST_ACCESS (ph[12]). A refusal is only noted in r->access_code and the next handler gets its chance; if nobody approves by the end, the POST_ACCESS slot finishes the request with the noted code. Under satisfy all, by contrast, NGX_OK means phase_handler++ (on to the next check), and a refusal is finished right away by the common code (lines 1185–1191).
Example 1 continued: slot order in the debug log
With the same config, nginx was started normally and the phase lines were picked out of the debug log for two requests. The date and pid at the start of each line were removed, and connection numbers such as *2 differ from run to run.
=== 2. debug log: GET /index.html ===
status=200
*2 generic phase: 0
*2 rewrite phase: 1
*2 test location: "/"
*2 test location: "admin/"
*2 using configuration "/"
*2 rewrite phase: 3
*2 post rewrite phase: 4
*2 generic phase: 5
...
*2 post access phase: 11
*2 generic phase: 12
*2 generic phase: 13
*2 content phase: 14
*2 content phase: 15
*2 content phase: 16
*2 content phase: 17
*2 http finalize request: 0, "/index.html?" a:1, c:1
*2 http log handler
=== 3. debug log: GET /admin/ (deny all) ===
status=403
...
*3 using configuration "/admin/"
*3 rewrite phase: 3
*3 post rewrite phase: 4
*3 generic phase: 5
*3 generic phase: 6
*3 generic phase: 7
*3 access phase: 8
*3 access forbidden by rule, client: 127.0.0.1, server: , request: "GET /admin/ HTTP/1.1", host: "127.0.0.1:8080"
*3 http finalize request: 403, "/admin/?" a:1, c:1
*3 http finalize request: 0, "/admin/?" a:1, c:1
(... marks where lines of the same shape, with the slot number going up by one each time, or lines identical to the previous request, were cut.)
- Lines such as
generic phase: Nare printed by the checkers (for example lines 925–926). The number is not the phase number but the ph[] slot number (r->phase_handler), so it matches Example 1's array exactly. Thefind_configslot (2) printstest locationandusing configurationinstead of a number. /index.htmlpasses through all ofcontent phase: 14to17. index, autoindex and gzip_static each returnedNGX_DECLINED, and static, the last one, sent the file. How a static file goes out is covered in Part 10./admin/ends ataccess phase: 8. The access module returned 403, the checker finished the request immediately, and auth_basic (9) and auth_request (10) were never called. "access forbidden by rule" is the line the access module prints undersatisfy all(ngx_http_access_module.clines 283–286).- The final
http log handleris the LOG phase. Outside ph[],ngx_http_free_request()(the call is at line 4029), which releases the request, callsngx_http_log_request(), and that walks the originalphases[NGX_HTTP_LOG_PHASE]array as it is (ngx_http_request.clines 4084–4089). Success or error, whichever slot it ended in, the request is logged once when it is released (as long asr->loggedis still 0; a subrequest is logged separately insidengx_http_finalize_request(), and only withlog_subrequest on— lines 2830–2838).
Easy to get wrong: 404 when nobody answers, but 403 for a trailing "/"
If every slot up to the last CONTENT slot returns NGX_DECLINED, the content checker finishes the request itself. If the URI ends in /, it logs "directory index of ... is forbidden" and returns 403; otherwise it logs "no handler found" and returns 404 (lines 1334–1351). That is why a directory without index.html gives 403 rather than 404. The ordinary 404 for a missing file is decided not here but by the static module when it fails to open the file.
4. Finding the location — a tree at config time, a walk down it per request
Now we go inside the FIND_CONFIG slot. "Who wins when =, ^~, ~ and plain prefixes overlap" lives here.
4-1. One modifier character becomes a flag
When a location block is met, ngx_http_core_location() creates a new loc_conf array using every module's create_loc_conf (lines 3174–3205) and reads the modifier to set flags in the core module's configuration (lines 3209–3301).
| Config | Flag | Meaning |
|---|---|---|
location = /img |
exact_match = 1 |
only when the URI is exactly equal |
location ^~ /img/static/ |
noregex = 1 |
a prefix; if it is the longest prefix, regexes are not looked at |
location ~ \.png$, ~* |
regex != NULL |
a regex (compiled when the config is read). ~* ignores case |
location $is_beta |
predicate = variable index + 1 |
a predicate location, new in 1.31.x |
location @fallback |
named = 1 |
a location found only by name |
location /img/ |
(none) | a plain prefix |
The predicate location, whose name starts with $, is a new feature in this version. It came in with commit 5f54125dd ("Predicate locations", authored 2026-03-29, committed 2026-08-26), and the word predicate does not appear at all in ngx_http_core_module.h of the 1.30.6 stable release. The commit message explains: "a name starting with $ is treated as a string evaluated at request time, and it matches if the value is neither empty nor "0". It is tried after prefix and regex locations." In the source, though, everything after the $ is taken as a single variable name: ngx_http_get_variable_index() turns it into an index, stored in predicate plus one (lines 3279–3291). We check that against the source and an example below.
Then ngx_http_add_location() puts it into the parent's locations queue, splitting into two kinds (lines 909–921). Plain prefixes (including ^~) go to inclusive ("takes every URI starting with that name"); everything else (=, regexes, predicates, named and nameless ones) goes to exact.
4-2. Sort, split off, build a tree
Once the http block has been read, ngx_http_block() calls ngx_http_init_locations() and ngx_http_init_static_location_trees() for each server (ngx_http.c lines 280–291).
- Sort: the comparison function
ngx_http_cmp_locations()(lines 970–1055) lines the queue up as prefixes and exact matches (by name) → regexes → predicates → named (by name) → nameless (ifblocks and the like). For regexes and predicates it returns 0 with a "do not sort" comment, and sincengx_queue_sort()is a stable merge sort, the order written in the config is kept. - Split off:
ngx_http_init_locations()cuts the named, predicate and regex groups off the back of the sorted queue and turns each into an array of pointers —cscf->named_locations,pclcf->predicate_locations,pclcf->regex_locations(lines 756–826). Nested locations get the same treatment recursively (line 709). - Build the tree: among the remaining prefixes and exact matches, an
=and a prefix with the same name are merged into one entry (two of the same kind is a "duplicate location" error), locations that start with a given prefix are moved underneath it, and a balanced binary tree is built with the middle of the sorted list as its root (lines 1058–1233).
struct ngx_http_location_tree_node_s {
ngx_http_location_tree_node_t *left;
ngx_http_location_tree_node_t *right;
ngx_http_location_tree_node_t *tree;
ngx_http_core_loc_conf_t *exact;
ngx_http_core_loc_conf_t *inclusive;
u_short len;
u_char auto_redirect;
u_char name[1];
};
src/http/ngx_http_core_module.h lines 478–489
left and right make an ordinary binary search tree; the unusual part is the third child, tree. Locations starting with /img/ form their own tree under the /img/ node's tree, and those nodes store in name only what is left after removing the part the parent already matched (prefix + len, line 1227). A node can hold both exact and inclusive, so location = /x and location /x share one node. name[1] is an old C idiom for allocating the name at the end of the struct (lines 1177–1178).
The tree built from Example 2's config. Yellow boxes are nodes built at config time; name is what is left after removing the prefix the parent matched.
4-3. Per request: walking down the tree
The loop of ngx_http_core_find_static_location() (the debug log line is elided):
for ( ;; ) {
if (node == NULL) {
return rv;
}
/* ... "test location: \"%*s\"" debug log ... */
n = (len <= (size_t) node->len) ? len : node->len;
rc = ngx_filename_cmp(uri, node->name, n);
if (rc != 0) {
node = (rc < 0) ? node->left : node->right;
continue;
}
if (len > (size_t) node->len) {
if (node->inclusive) {
r->loc_conf = node->inclusive->loc_conf;
rv = NGX_AGAIN;
node = node->tree;
uri += n;
len -= n;
continue;
}
/* exact only */
node = node->right;
continue;
}
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;
}
}
/* ... len < node->len: check auto_redirect, then go left ... */
}
src/http/ngx_http_core_module.c lines 1557–1620
- If the leading part differs, go
leftorright. - If the node's name equals the start of the URI and the URI is longer, an
inclusivenode is hung onr->loc_conffor now (rv = NGX_AGAIN) and the walk goes down intotreewith the matched part removed. A longer prefix found further down overwrites it. This is how "the longest prefix wins" is implemented, and instead of comparing the string from the start each time, it compares only the remainder. - If the lengths are equal too, it returns
NGX_OK(exact match, done) when there is anexact, and otherwiseNGX_AGAINwith that prefix.
The comparison function ngx_filename_cmp() compares byte by byte, is case-sensitive (only builds for case-insensitive file systems apply tolower), and treats / as the smallest character (ngx_string.c lines 926–963). Because sorting and searching use the same function, the tree stays consistent.
4-4. Then the regexes, then the predicates
ngx_http_core_find_location(), which wraps the tree search, decides the rest of the rules.
rc = ngx_http_core_find_static_location(r, pclcf->static_locations);
if (rc == NGX_AGAIN) {
clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
noregex = clcf->noregex;
/* look up nested locations */
rc = ngx_http_core_find_location(r);
}
if (rc == NGX_OK || rc == NGX_DONE || rc == NGX_ERROR) {
return rc;
}
/* rc == NGX_DECLINED or rc == NGX_AGAIN in nested location */
#if (NGX_PCRE)
if (noregex == 0 && pclcf->regex_locations) {
/* ... ngx_http_regex_exec() in config order; on the first match swap r->loc_conf, NGX_OK ... */
}
#endif
if (noregex == 0 && pclcf->predicate_locations) {
for (clcfp = pclcf->predicate_locations; *clcfp; clcfp++) {
/* ... */
vv = ngx_http_get_flushed_variable(r, (*clcfp)->predicate - 1);
if (vv == NULL) {
return NGX_ERROR;
}
if (vv->len && (vv->len != 1 || vv->data[0] != '0')) {
r->loc_conf = (*clcfp)->loc_conf;
/* ... look up nested locations, then NGX_OK ... */
}
}
}
return rc;
src/http/ngx_http_core_module.c lines 1460–1533
In short:
- An exact match (
=) in the prefix tree ends it. - On a prefix match, nested locations inside that location are searched first, recursively.
- If the longest prefix has
^~(noregex), both regexes and predicates are skipped. - Otherwise regexes are tried in config order and the first match is used. Even after finding the longest prefix, a matching regex wins.
- If no regex matches, predicates are evaluated in config order and the first true one is used.
- If there is still nothing, the prefix hung up in step 2 remains; if there was no prefix either, the server's default configuration stays.
Green boxes mean "decided here". For regexes and predicates, the order in the config is the priority.
Example 2: who wins among overlapping locations
This is the inside of http {} from the config in the example folder ex/u09-location-match. The response body tells which location was chosen.
# $is_beta is "1" with ?beta=1, otherwise "0"
map $arg_beta $is_beta {
default 0;
1 1;
}
server {
listen 8080;
location = /img { return 200 "= /img\n"; }
location /img/ { return 200 "/img/\n"; }
location /img/icons/ { return 200 "/img/icons/\n"; }
location ^~ /img/static/ { return 200 "^~ /img/static/\n"; }
location ~ \.png$ { return 200 "~ regex (png)\n"; }
# predicate location, new in 1.31.x (not in 1.30.x)
location $is_beta { return 200 "predicate (is_beta=$is_beta)\n"; }
location / { return 200 "/\n"; }
}
run.sh sends ten URIs with curl, then picks only the test location and using configuration lines out of the debug log for three requests.
=== 1. which location wins ===
/img -> = /img
/img/a.jpg -> /img/
/img/a.png -> ~ regex (png)
/img/icons/a.gif -> /img/icons/
/img/icons/a.png -> ~ regex (png)
/img/static/a.png -> ^~ /img/static/
/other -> /
/other?beta=1 -> predicate (is_beta=1)
/img/a.png?beta=1 -> ~ regex (png)
/img/a.jpg?beta=1 -> predicate (is_beta=1)
=== 2. debug log: GET /img/icons/a.png ===
*12 test location: "/"
*12 test location: "img/"
*12 test location: "static/"
*12 test location: "icons/"
*12 test location: ~ "\.png$"
*12 using configuration "\.png$"
=== 3. debug log: GET /img/static/a.png ===
*13 test location: "/"
*13 test location: "img/"
*13 test location: "static/"
*13 using configuration "/img/static/"
=== 4. debug log: GET /other?beta=1 ===
*14 test location: "/"
*14 test location: "img/"
*14 test location: ~ "\.png$"
*14 test location: "$is_beta"
*14 using configuration "$is_beta"
/img/icons/a.png: the walk reached the longest prefix/img/icons/, but it is not^~, so the regex was tried and the regex won. "The longest prefix wins" is only true when no regex matches./img/static/a.png: also ends in.png, yet^~ /img/static/won, and the log has notest location: ~at all.noregexskipped the regex loop.- The node names in the log,
"/","img/","static/","icons/", are the trimmed names from section 4-2. Underimg/,static/was looked at first because it is the root of that small tree (for a two-element listngx_queue_middle()picks the second,ngx_queue.clines 21–49). - Predicates come after regexes. For
/img/a.png?beta=1the regex matched first and the predicate was not looked at; for/img/a.jpg?beta=1no regex matched and the predicate beat the prefix/img/. The commit message, the source and the run all agree.
Easy to get wrong: where config order matters and where it doesn't
Prefix locations are sorted into a tree, so the order you write them in means nothing. Example 2 wrote location / last, and it would give the same results anywhere. Regex and predicate locations are the opposite: the order you write them in is their priority. Put location ~ /img/ above location ~ \.png$ and /img/a.png goes to the upper one.
5. The rewrite module and variables — a small interpreter plugged into a phase
The rewrite module handles rewrite, return, set, if and break. These directives are compiled into a "code array" when the config is read, and at request time the array is run from the start.
e->ip = rlcf->codes->elts;
e->request = r;
e->quote = 1;
e->log = rlcf->log;
e->status = NGX_DECLINED;
while (*(uintptr_t *) e->ip) {
code = *(ngx_http_script_code_pt *) e->ip;
code(e);
}
return e->status;
}
src/http/modules/ngx_http_rewrite_module.c lines 172–184
e->ip points at "the code to run now" (the name comes from a CPU's instruction pointer). Each entry of the array is a struct that starts with a function pointer; the loop just calls that function, and each code function moves e->ip forward by its own size. It stops at a slot that is 0 (NULL). If e->status is still its initial NGX_DECLINED, the rewrite checker moves on to the next slot.
The line rewrite ^/a/(.*)$ /b/$1 last; becomes a regex start code, copy codes that build the replacement string, and a regex end code. A fourth argument of last sets last = 1; break sets regex->break_cycle = 1 and last = 1 (lines 363–386). With last = 1 one more NULL code is appended after the end code (lines 425–432), so if the regex matches, the location's remaining rewrite directives don't run. Up to here the two are the same; the difference is one flag set at request time.
if (code->uri) {
r->internal = 1;
r->valid_unparsed_uri = 0;
if (code->break_cycle) {
r->valid_location = 0;
r->uri_changed = 0;
} else {
r->uri_changed = 1;
}
}
src/http/ngx_http_script.c lines 1179–1190
last:r->uri_changed = 1. The POST_REWRITE checker in the next slot sees it and sends us back to FIND_CONFIG. The location is looked up again with the new URI.break:r->uri_changed = 0. POST_REWRITE just passes, and the request stays in the current location with only the URI changed.
Before sending us back, the POST_REWRITE checker counts.
r->uri_changes--;
if (r->uri_changes == 0) {
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
"rewrite or internal redirection cycle "
"while processing \"%V\"", &r->uri);
ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
return NGX_OK;
}
r->phase_handler = ph->next;
cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module);
r->loc_conf = cscf->ctx->loc_conf;
return NGX_AGAIN;
}
src/http/ngx_http_core_module.c lines 1098–1115
r->uri_changes starts at NGX_HTTP_MAX_URI_CHANGES + 1, that is 11, when the request is created (ngx_http_request.c line 661; the constant 10 is at ngx_http_request.h line 12). When it reaches 0 the result is 500. r->loc_conf is reset to the server's default before going back because the location has to be found from scratch.
Example 3: last, break, a named location and an endless rewrite
The server {} block from the example folder ex/u09-rewrite-redirect. The root contains just one file, /b/x.txt ("static file /b/x.txt").
server {
listen 8080;
# server-level rewrite (runs in the SERVER_REWRITE phase)
rewrite ^/old/(.*)$ /b/$1 last;
location /a/ {
# last: change the URI and look up the location again
rewrite ^/a/(.*)$ /b/$1 last;
}
location /brk/ {
# break: only change the URI and stay in this location (the return below never runs)
rewrite ^/brk/(.*)$ /b/$1 break;
return 200 "never reached\n";
}
location /b/ {
return 200 "location /b/: uri=$uri request_uri=$request_uri\n";
}
location /loop/ {
# keeps rewriting to itself → 500 after 10 times
rewrite ^/loop/(.*)$ /loop/$1 last;
}
location /try/ {
try_files $uri @fallback;
}
location @fallback {
return 200 "named @fallback: uri=$uri\n";
}
}
=== 1. responses ===
/a/x.txt -> location /b/: uri=/b/x.txt request_uri=/a/x.txt [200]
/brk/x.txt -> static file /b/x.txt [200]
/old/x.txt -> location /b/: uri=/b/x.txt request_uri=/old/x.txt [200]
/try/none.txt -> named @fallback: uri=/try/none.txt [200]
/loop/x -> (error page) [500]
/a/x.txt(last) was answered bylocation /b/.$uriis the changed/b/x.txt;$request_uriis what the client sent./brk/x.txt(break) stayed inlocation /brk/. Thereturnbelow it never ran (the code array stopped atNULL), and the static module in the CONTENT phase sent the file for the changed URI./old/x.txtwas changed at server level. SERVER_REWRITE comes before FIND_CONFIG, so there is no need to go back.
Putting last and break side by side in the debug log makes the difference plain.
=== 2. debug log: GET /a/x.txt (rewrite ... last) ===
*7 generic phase: 0
*7 rewrite phase: 1
*7 http script regex: "^/old/(.*)$"
*7 test location: "/brk/"
*7 test location: "/b/"
*7 test location: "/a/"
*7 using configuration "/a/"
*7 rewrite phase: 3
*7 http script regex: "^/a/(.*)$"
*7 http script regex end
*7 rewritten data: "/b/x.txt", args: ""
*7 post rewrite phase: 4
*7 uri changes: 11
*7 test location: "/brk/"
*7 test location: "/b/"
*7 using configuration "/b/"
*7 rewrite phase: 3
*7 http finalize request: 0, "/b/x.txt?" a:1, c:1
=== 3. debug log: GET /brk/x.txt (rewrite ... break) ===
...
*8 using configuration "/brk/"
*8 rewrite phase: 3
*8 http script regex: "^/brk/(.*)$"
*8 http script regex end
*8 rewritten data: "/b/x.txt", args: ""
*8 post rewrite phase: 4
*8 generic phase: 5
...
*8 content phase: 17
*8 http finalize request: 0, "/b/x.txt?" a:1, c:1
lastprinteduri changes: 11afterpost rewrite phase: 4and started again fromtest location. In the new location/b/,rewrite phase: 3ran again and thereturnfinished the request.breaksimply passedpost rewrite phase: 4on togeneric phase: 5, andusing configurationappears only once.- Both first tried the server-level
^/old/inrewrite phase: 1(no match).
The two debug logs above, side by side. The only difference is whether post rewrite phase sends the request back.
The endless rewrite (/loop/x) prints eleven lines from uri changes: 11 down to uri changes: 1, then ends like this.
*10 uri changes: 2
*10 uri changes: 1
*10 rewrite or internal redirection cycle while processing "/loop/x"
The log prints the value before decrementing. From 11 down to 2 it went back ten times, and on the eleventh, which printed 1, the count hit 0 and gave 500. Processing again with a changed URI is allowed up to ten times per request, and that count is shared with the internal redirects and named locations of section 6.
Variables — names become numbers at config time, values are computed once per request
return 200 "... uri=$uri ..." is also compiled at config time. The name $uri becomes an integer index through ngx_http_get_variable_index(). A variable is defined by a table of names and "functions that compute the value".
{ ngx_string("uri"), NULL, ngx_http_variable_request,
offsetof(ngx_http_request_t, uri),
NGX_HTTP_VAR_NOCACHEABLE, 0 },
src/http/ngx_http_variables.c lines 247–249
This is one line of ngx_http_core_variables[] (from line 168). ngx_http_variable_request returns the ngx_str_t at the offset it receives as data (here r->uri). Looking at ngx_http_get_indexed_variable() (lines 618–665), which fetches the value at request time, each request has an r->variables[] array, and the value is computed with get_handler only the first time it is asked for and stored in that slot. After that it is returned straight away by checking the valid bit — lazy evaluation plus a cache. $uri, though, carries NGX_HTTP_VAR_NOCACHEABLE. The source doesn't write down why, but rewrites and internal redirects change r->uri in the middle of a request, so reusing a value computed once could be wrong. ngx_http_get_flushed_variable() (lines 668–685) clears such slots and recomputes them, and the predicate locations of section 4-4 fetched their values through this function.
6. Again with another URI — internal redirects and named locations
Besides rewrite ... last there are two more ways to "go again with another URI", and the three go back to different slots of ph[].
ngx_http_internal_redirect() decrements and checks uri_changes (lines 2625–2635), changes the URI, and starts over.
/* clear the modules contexts */
ngx_memzero(r->ctx, sizeof(void *) * ngx_http_max_module);
cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module);
r->loc_conf = cscf->ctx->loc_conf;
ngx_http_update_location_config(r);
/* ... */
r->internal = 1;
r->valid_unparsed_uri = 0;
r->add_uri_to_alias = 0;
r->main->count++;
ngx_http_handler(r);
return NGX_DONE;
}
src/http/ngx_http_core_module.c lines 2651–2671
It clears the state modules attached to this request (r->ctx), resets r->loc_conf to the server default, and with r->internal = 1 calls ngx_http_handler() again. As section 3 showed, an internal request runs from server_rewrite_index; POST_READ is not repeated. Also worth noting: it does not create a new ngx_http_request_t but reuses the same struct. httpd created a new request_rec for each internal redirect and linked them with r->prev (httpd Part 10, section 6). Callers include the index module, try_files ending in a URI, error_page with a URI target (ngx_http_special_response.c line 631), and upstream's X-Accel-Redirect (ngx_http_upstream.c line 3163).
Requesting / from Example 1's server makes the index module do an internal redirect to /index.html.
=== 4. debug log: GET / (index -> internal redirect) ===
status=200
...
*4 generic phase: 13
*4 content phase: 14
*4 internal redirect: "/index.html?"
*4 rewrite phase: 1
*4 test location: "/"
*4 test location: "admin/"
*4 using configuration "/"
*4 rewrite phase: 3
...
*4 content phase: 17
*4 http finalize request: 0, "/index.html?" a:1, c:2
*4 http finalize request: -4, "/index.html?" a:1, c:1
The internal redirect happens at content phase: 14 (index), and the next line is rewrite phase: 1. generic phase: 0 does not appear again. In the first round, / ended after a single test location: "/" (the URI is as long as the node name, so NGX_AGAIN); in the second round, /index.html is longer, so it went down into tree, compared against admin/, and stayed with /. The c:2 → c:1 at the end is the r->main->count (reference count) that the redirect raised coming back down.
ngx_http_named_location() finds by name, so it skips FIND_CONFIG. Once it finds the name in cscf->named_locations (split off in section 4-2), it switches r->loc_conf straight to that location and runs from r->phase_handler = cmcf->phase_engine.location_rewrite_index (3 in Example 1) (lines 2720–2735). The URI is not changed. In Example 3's /try/none.txt log, this is what follows try_files in PRECONTENT (generic phase: 12).
*9 generic phase: 12
*9 test location: "@fallback"
*9 using location: @fallback "/try/none.txt?"
*9 rewrite phase: 3
*9 http finalize request: 0, "/try/none.txt?" a:1, c:2
It goes straight to rewrite phase: 3 without a using configuration (FIND_CONFIG's log line), and the response's $uri was still the original /try/none.txt.
Slot numbers follow Example 1's array. All three ways of going back decrement r->uri_changes by one and end in 500 when it reaches 0.
Easy to get wrong: an internal redirect repeats the access checks
Going back to an earlier slot means passing through every phase after it again. The / → /index.html redirect went through the ACCESS slots again too. httpd skipped access checks when the configuration object was the same, but nginx's access checker skips them only for subrequests (r != r->main) (lines 1125–1128). An internal redirect reuses the same struct (r == r->main), so the checks run again.
What this post leaves out
- The 301 auto redirect when
/dircomes in forlocation /dir/(auto_redirect):ngx_http_core_module.clines 1031–1067, lines 1612–1617 - The
client_max_body_sizecheck (inside FIND_CONFIG): lines 1016–1029 limit_exceptblocks (a per-methodloc_conf):ngx_http_update_location_config()lines 1361–1364auth_delay:ngx_http_core_auth_delay()lines 1229–1271- The rewrite module's
if(a nameless location,noname) andset:ngx_http_rewrite_if(),ngx_http_rewrite_set() - The script engine's length codes and copy codes:
ngx_http_script_compile()inngx_http_script.c(from line 468) - Subrequests (
ngx_http_subrequest()) and the postpone filter: in Part 10 post_action:ngx_http_request.clines 3921–3940
Recap
| Concept | One-line summary |
|---|---|
| phase | 11 slots that divide request processing. The core sets the order; modules put functions into cmcf->phases[i].handlers |
| ph[] array | the per-phase arrays flattened into one at config time. Each slot has checker, handler, next; the end is checker == NULL |
| copied backwards | within a phase, modules later in the module list are called first (index before static) |
ngx_http_core_run_phases() |
a loop that calls the checker of slot r->phase_handler until one returns NGX_OK. After a pause it resumes at the same slot |
| checker | reads handler return values differently per phase (OK also ends it for rewrite; access depends on satisfy) |
| LOG phase | outside ph[]. ngx_http_log_request() calls it when the request is released |
| location tree | prefixes sorted into a binary tree, with shared prefixes as tree children. Node names are what is left after the prefix |
| priority | = → longest prefix (nested first) → stop if ^~ → regexes (config order) → predicates (config order, 1.31.x) → longest prefix |
| content handler | proxy_pass and friends plant it in clcf->handler; after FIND_CONFIG it becomes r->content_handler |
| rewrite module | compiled into a code array at config time, run by a function-pointer loop per request. last goes to FIND_CONFIG, break stays |
| variables | names become indexes at config time. Values are computed on first use and cached; changing ones like $uri are recomputed |
| internal redirect | reuses the same struct, from server_rewrite_index. Named locations start at location_rewrite_index. Ten times in total |
What to look at next
Once a handler has been chosen in the CONTENT phase, the response is produced and flows out. The static module opens the file, puts it in an ngx_buf_t and hands it to ngx_http_send_header() and ngx_http_output_filter(), after which the header filter, gzip, chunked and the write filter follow as a chain of function pointers. That is what happened between content phase: 17 and http finalize request in this post's examples. It continues in Part 10: Filters and Static Files — How a Response Flows Out. What happens after proxy_pass becomes the content handler is in Part 11.
Other servers solve the same problem like this. httpd steps through phases with named hooks and RUN_FIRST/RUN_ALL rules, and for every request does a "walk" that turns the URL into a disk path and <Directory> settings (httpd Part 10: Request Processing). Tomcat's Mapper also keeps names sorted and finds the longest path by binary search — the same idea as nginx's prefix tree, except that it searches again after cutting the URI back one segment at a time (Tomcat Part 7: CoyoteAdapter and the Mapper). The chain a request passes through is worth comparing with Tomcat Part 8's pipelines and valves, and turning a URL into a Java method with Spring Part 22: Handler Mapping and Adapters.