
Nginx C Modules
- 248 installs
- 191 repo stars
- Updated July 24, 2026
- pproenca/dot-skills
nginx-c-modules: A skill for development. This provides functionality for development workflows.
Key points
- nginx-c-modules
Nginx C Modules by the numbers
- 248 all-time installs (skills.sh)
- +10 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,525 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/pproenca/dot-skills --skill nginx-c-modulesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 248 |
|---|---|
| repo stars | ★ 191 |
| Last updated | July 24, 2026 |
| Repository | pproenca/dot-skills ↗ |
How do I use nginx-c-modules for development tasks?
Use nginx-c-modules for development tasks
Who is it for?
Best when you're working on backend & apis and need structured help with nginx-c-modules.
Skip if: Teams with no backend & apis needs, or anyone wanting a generic chat assistant without this specific workflow.
When should I use this skill?
When you need to use nginx-c-modules for development tasks, or when nginx-c-modules: a skill for development. this provides functionality for development workflows.
What you get
Structured output aligned to nginx-c-modules: nginx-c-modules.
Files
nginx.org C Module Development Best Practices
Comprehensive development guide for nginx C modules, derived from the official nginx development documentation and community expertise. Contains 49 rules across 8 categories, prioritized by impact to guide correct module implementation and prevent common crashes, memory leaks, and undefined behavior.
When to Apply
Reference these guidelines when:
- Writing new nginx C modules (handlers, filters, upstream, load-balancers)
- Implementing configuration directives and merge logic
- Managing memory with nginx pools and shared memory zones
- Handling the HTTP request lifecycle (body reading, subrequests, finalization)
- Working with nginx's event loop, timers, and thread pools
Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Memory Management | CRITICAL | mem- |
| 2 | Request Lifecycle | CRITICAL | req- |
| 3 | Configuration System | HIGH | conf- |
| 4 | Handler Development | HIGH | handler- |
| 5 | Filter Chain | MEDIUM-HIGH | filter- |
| 6 | Upstream & Proxy | MEDIUM | upstream- |
| 7 | Event Loop & Concurrency | MEDIUM | event- |
| 8 | Data Structures & Strings | LOW-MEDIUM | ds- |
Quick Reference
1. Memory Management (CRITICAL)
- `mem-pool-allocation` - Use Pool Allocation Instead of Heap malloc
- `mem-check-allocation` - Check Every Allocation Return for NULL
- `mem-pcalloc-structs` - Use ngx_pcalloc for Struct Initialization
- `mem-cleanup-handlers` - Register Pool Cleanup Handlers for External Resources
- `mem-pnalloc-strings` - Use ngx_pnalloc for String Data Allocation
- `mem-pfree-limitations` - Avoid Relying on ngx_pfree for Pool Allocations
- `mem-shared-slab` - Use Slab Allocator for Shared Memory Zones
2. Request Lifecycle (CRITICAL)
- `req-finalize-once` - Finalize Requests Exactly Once
- `req-no-access-after-finalize` - Never Access Request After Finalization
- `req-body-async` - Handle Request Body Reading Asynchronously
- `req-discard-body` - Discard Request Body When Not Reading It
- `req-subrequest-completion` - Use Post-Subrequest Handlers for Completion
- `req-count-reference` - Increment Request Count Before Async Operations
- `req-internal-redirect` - Return After Internal Redirect
3. Configuration System (HIGH)
- `conf-unset-init` - Initialize Config Fields with UNSET Constants
- `conf-merge-all-fields` - Merge All Config Fields in merge_loc_conf
- `conf-context-flags` - Use Correct Context Flags for Directives
- `conf-null-command` - Terminate Commands Array with ngx_null_command
- `conf-custom-handler` - Use Custom Handlers for Complex Directive Parsing
- `conf-module-ctx-null` - Set Unused Module Context Callbacks to NULL
- `conf-build-config` - Write Correct config Build Script for Module Compilation
4. Handler Development (HIGH)
- `handler-send-header-first` - Send Header Before Body Output
- `handler-last-buf` - Set last_buf Flag on Final Buffer
- `handler-phase-registration` - Register Phase Handlers in postconfiguration
- `handler-content-handler` - Use content_handler for Location-Specific Response Generation
- `handler-error-page` - Return HTTP Status Codes for Error Responses
- `handler-empty-response` - Use header_only for Empty Body Responses
- `handler-module-ctx` - Use Module Context for Per-Request State
- `handler-add-variable` - Register Custom Variables in preconfiguration
5. Filter Chain (MEDIUM-HIGH)
- `filter-registration-order` - Save and Replace Top Filter in postconfiguration
- `filter-call-next` - Always Call Next Filter in the Chain
- `filter-check-subrequest` - Distinguish Main Request from Subrequest in Filters
- `filter-buffer-chain-iteration` - Iterate Buffer Chains Using cl->next Pattern
- `filter-buffering-flag` - Set Buffering Flag When Accumulating Response Data
6. Upstream & Proxy (MEDIUM)
- `upstream-create-request` - Build Complete Request Buffer in create_request
- `upstream-process-header` - Parse Upstream Response Incrementally in process_header
- `upstream-peer-free` - Track Failures in Peer free Callback
- `upstream-finalize` - Clean Up Resources in finalize_request Callback
- `upstream-connection-reuse` - Enable Keepalive for Upstream Connections
7. Event Loop & Concurrency (MEDIUM)
- `event-no-blocking` - Never Use Blocking Calls in Event Handlers
- `event-timer-management` - Delete Timers Before Freeing Associated Data
- `event-handle-read-write` - Call ngx_handle_read/write_event After I/O Operations
- `event-thread-pool` - Offload Blocking Operations to Thread Pool
- `event-posted-events` - Use Posted Events for Deferred Processing
8. Data Structures & Strings (LOW-MEDIUM)
- `ds-ngx-str-not-null-terminated` - Never Assume ngx_str_t Is Null-Terminated
- `ds-ngx-str-set-literals` - Use ngx_string Macro Only with String Literals
- `ds-cpymem-pattern` - Use ngx_cpymem for Sequential Buffer Writes
- `ds-list-iteration` - Iterate ngx_list_t Using Part-Based Pattern
- `ds-hash-readonly` - Build Hash Tables During Configuration Only
How to Use
Read individual reference files for detailed explanations and code examples:
- Section definitions - Category structure and impact levels
- Rule template - Template for adding new rules
Reference Files
| File | Description |
|---|---|
| references/_sections.md | Category definitions and ordering |
| assets/templates/_template.md | Template for new rules |
| metadata.json | Version and reference information |
Rule Title Here
1-3 sentences explaining WHY this matters. Focus on the consequences of getting it wrong in nginx module development.
Incorrect (what's wrong):
/* code example showing the anti-pattern */
/* comments explain the cost/consequence */Correct (what's right):
/* code example showing the correct approach */
/* comments explain the benefit */Note: Optional additional context, exceptions, or related patterns.
Reference: Reference Title
{
"version": "1.0.6",
"organization": "nginx.org",
"technology": "nginx C Modules",
"date": "February 2026",
"abstract": "Comprehensive development guide for nginx C modules, designed for AI agents and LLMs. Contains 49 rules across 8 categories, prioritized by impact from critical (memory management, request lifecycle) to incremental (data structures and strings). Each rule includes detailed explanations, real-world C code examples comparing incorrect vs. correct implementations, and specific impact metrics to guide correct module development.",
"references": [
"https://nginx.org/en/docs/dev/development_guide.html",
"https://www.evanmiller.org/nginx-modules-guide.html",
"https://www.evanmiller.org/nginx-modules-guide-advanced.html",
"https://github.com/nginx/nginx",
"https://github.com/baishancloud/nginx-development-guide",
"https://www.nginx.com/resources/wiki/extending/api/"
]
}
Sections
This file defines all sections, their ordering, impact levels, and descriptions. The section ID (in parentheses) is the filename prefix used to group rules.
---
1. Memory Management (mem)
Impact: CRITICAL Description: Pool misuse causes segfaults and memory leaks; every request allocates from pools. Wrong allocation strategy cascades into worker crashes under load.
2. Request Lifecycle (req)
Impact: CRITICAL Description: Misunderstanding request lifecycle causes use-after-free, double finalize, and accessing freed data. Subrequests and reference counting add multiplicative complexity.
3. Configuration System (conf)
Impact: HIGH Description: Wrong UNSET initialization breaks config inheritance across server/location blocks. Bad merge logic causes silent undefined runtime behavior.
4. Handler Development (handler)
Impact: HIGH Description: Content and phase handlers are the core module work. Wrong response generation corrupts HTTP output or crashes active connections.
5. Filter Chain (filter)
Impact: MEDIUM-HIGH Description: Filters process every response in the pipeline. Registration order bugs, buffer chain mismanagement, and missing flags corrupt output or cause infinite loops.
6. Upstream & Proxy (upstream)
Impact: MEDIUM Description: Upstream callback errors cause proxy failures, connection leaks, and retry storms. The complex state machine requires correct callback ordering.
7. Event Loop & Concurrency (event)
Impact: MEDIUM Description: Blocking calls in handlers freeze entire workers serving thousands of connections. Timer and thread pool misuse introduces resource leaks and race conditions.
8. Data Structures & Strings (ds)
Impact: LOW-MEDIUM Description: ngx_str_t is NOT null-terminated — the #1 source of bugs for newcomers. Container iteration patterns and hash table immutability differ from standard C idioms.
Write Correct config Build Script for Module Compilation
Every nginx C module requires a config shell script in its root directory. This script tells the nginx build system how to compile and link the module. The script format differs between static modules (compiled into the nginx binary) and dynamic modules (compiled as .so shared objects loaded at runtime). A missing or malformed config script causes silent build failures or modules that compile but cannot be loaded.
Incorrect (missing required variables for dynamic module support):
# config — only works with static compilation
ngx_addon_name=ngx_http_mymodule_module
HTTP_MODULES="$HTTP_MODULES ngx_http_mymodule_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_mymodule_module.c"
# BUG: --add-dynamic-module will fail — missing ngx_module_type,
# ngx_module_name, ngx_module_srcs, and . auto/moduleCorrect (supports both static and dynamic module compilation):
# config — works with both --add-module and --add-dynamic-module
ngx_addon_name=ngx_http_mymodule_module
if test -n "$ngx_module_link"; then
# nginx 1.9.11+ dynamic module support
ngx_module_type=HTTP
ngx_module_name=ngx_http_mymodule_module
ngx_module_srcs="$ngx_addon_dir/ngx_http_mymodule_module.c"
. auto/module
else
# legacy static module support
HTTP_MODULES="$HTTP_MODULES ngx_http_mymodule_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_mymodule_module.c"
fiNote: For dynamic modules, the compiled .so file is loaded with load_module modules/ngx_http_mymodule_module.so; in nginx.conf. Use ngx_module_type=HTTP for HTTP modules, STREAM for stream modules, or HTTP_FILTER for filter modules. If your module has multiple source files, list them all in ngx_module_srcs separated by spaces. Add external library dependencies via ngx_module_libs="-lssl -lcrypto".
Reference: nginx Development Guide — Building
Use Correct Context Flags for Directives
Each directive declaration specifies which configuration contexts it is valid in via bitmask flags. Wrong flags allow a directive to appear in an unexpected block (e.g., only http {} when it should work per-location), or reject it from blocks where it is needed, leading to config parse errors or crashes when the handler reads from the wrong config level.
Incorrect (main-only flag for a directive that should work per-location):
static ngx_command_t ngx_http_mymodule_commands[] = {
{ ngx_string("mymodule_timeout"),
NGX_HTTP_MAIN_CONF|NGX_CONF_TAKE1,
ngx_conf_set_msec_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, timeout),
NULL },
/* BUG: NGX_HTTP_MAIN_CONF only — placing this directive inside
* a server{} or location{} block causes a config parse error,
* yet the slot reads from LOC_CONF_OFFSET */
ngx_null_command
};Correct (flags match the intended config levels):
static ngx_command_t ngx_http_mymodule_commands[] = {
{ ngx_string("mymodule_timeout"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_msec_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, timeout),
NULL },
ngx_null_command
};Note: Use NGX_HTTP_LOC_CONF_OFFSET with NGX_HTTP_LOC_CONF (and optionally parent levels for inheritance), NGX_HTTP_SRV_CONF_OFFSET with NGX_HTTP_SRV_CONF, and NGX_HTTP_MAIN_CONF_OFFSET with NGX_HTTP_MAIN_CONF. The offset level and context flags must agree.
Use Custom Handlers for Complex Directive Parsing
Built-in slot functions like ngx_conf_set_str_slot and ngx_conf_set_num_slot handle simple single-value directives. When a directive needs multiple arguments, cross-field validation, or conditional logic, a custom set handler is required. Forcing a built-in slot for complex cases leads to silent data loss or parse failures.
Incorrect (forcing built-in slot for a multi-value directive):
/* Directive: mymodule_rate_limit 100 burst=50 */
static ngx_command_t ngx_http_mymodule_commands[] = {
{ ngx_string("mymodule_rate_limit"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_num_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, rate),
NULL },
/* BUG: only reads first arg — "burst=50" is silently ignored
* and NGX_CONF_TAKE1 rejects the second argument entirely */
ngx_null_command
};Correct (custom handler parses and validates all arguments):
static char *
ngx_http_mymodule_rate_limit(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_mymodule_loc_conf_t *mlcf = conf;
ngx_str_t *value;
ngx_int_t rate;
value = cf->args->elts;
rate = ngx_atoi(value[1].data, value[1].len);
if (rate == NGX_ERROR || rate <= 0) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid rate \"%V\"", &value[1]);
return NGX_CONF_ERROR;
}
mlcf->rate = rate;
if (cf->args->nelts > 2
&& ngx_strncmp(value[2].data, "burst=", 6) == 0)
{
mlcf->burst = ngx_atoi(value[2].data + 6, value[2].len - 6);
if (mlcf->burst == NGX_ERROR) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid burst \"%V\"", &value[2]);
return NGX_CONF_ERROR;
}
}
return NGX_CONF_OK;
}Merge All Config Fields in merge_loc_conf
Every field set to an UNSET constant in create_loc_conf must have a corresponding merge macro call in merge_loc_conf. Missing merges leave UNSET sentinel values (typically -1) at runtime, causing integer overflow in timeout calculations, unsigned wraparound in size comparisons, or unexpected logic branches.
Incorrect (missing merge leaves UNSET value at runtime):
static char *
ngx_http_mymodule_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
{
ngx_http_mymodule_loc_conf_t *prev = parent;
ngx_http_mymodule_loc_conf_t *conf = child;
ngx_conf_merge_msec_value(conf->timeout, prev->timeout, 60000);
ngx_conf_merge_value(conf->enable, prev->enable, 0);
/* BUG: max_retries was set to NGX_CONF_UNSET_UINT in create_loc_conf
* but has no merge here — stays as (ngx_uint_t) -1 at runtime */
return NGX_CONF_OK;
}Correct (every UNSET field has a matching merge with a sensible default):
static char *
ngx_http_mymodule_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
{
ngx_http_mymodule_loc_conf_t *prev = parent;
ngx_http_mymodule_loc_conf_t *conf = child;
ngx_conf_merge_msec_value(conf->timeout, prev->timeout, 60000);
ngx_conf_merge_value(conf->enable, prev->enable, 0);
ngx_conf_merge_uint_value(conf->max_retries, prev->max_retries, 3);
return NGX_CONF_OK;
}Note: Match merge macros to UNSET types: ngx_conf_merge_value for NGX_CONF_UNSET, ngx_conf_merge_uint_value for NGX_CONF_UNSET_UINT, ngx_conf_merge_msec_value for NGX_CONF_UNSET_MSEC, ngx_conf_merge_size_value for NGX_CONF_UNSET_SIZE, and ngx_conf_merge_ptr_value for NGX_CONF_UNSET_PTR.
Set Unused Module Context Callbacks to NULL
The ngx_http_module_t structure has eight callback slots covering preconfiguration, postconfiguration, and create/merge for main, server, and location config. Unused slots must be explicitly set to NULL. While C zero-initializes trailing omitted fields in aggregate initializers, relying on this makes the struct fragile — reordering callbacks or adding a new slot silently leaves a function pointer unset, and the omission is invisible during review.
Incorrect (partially initialized — intent unclear, error-prone on refactoring):
static ngx_http_module_t ngx_http_mymodule_module_ctx = {
NULL, /* preconfiguration */
ngx_http_mymodule_postconfiguration, /* postconfiguration */
NULL, /* create main configuration */
NULL, /* init main configuration */
NULL, /* create server configuration */
NULL, /* merge server configuration */
ngx_http_mymodule_create_loc_conf /* create location configuration */
/* merge_loc_conf slot omitted — C zero-initializes trailing fields,
* but the missing slot is invisible to reviewers and breaks silently
* if callbacks are reordered or a new one is needed */
};Correct (all eight callbacks explicitly declared):
static ngx_http_module_t ngx_http_mymodule_module_ctx = {
NULL, /* preconfiguration */
ngx_http_mymodule_postconfiguration, /* postconfiguration */
NULL, /* create main configuration */
NULL, /* init main configuration */
NULL, /* create server configuration */
NULL, /* merge server configuration */
ngx_http_mymodule_create_loc_conf, /* create location configuration */
ngx_http_mymodule_merge_loc_conf /* merge location configuration */
};Note: In C, trailing omitted initializers in aggregate types are zero-initialized only when the struct is defined with an initializer list. However, relying on this is fragile — explicitly listing all eight slots documents intent and prevents mistakes when callbacks are reordered.
Terminate Commands Array with ngx_null_command
nginx iterates the commands array sequentially until it finds a zero-filled sentinel entry. A missing ngx_null_command terminator causes the parser to read past the end of the array into adjacent memory, interpreting garbage as command definitions. This produces cryptic config errors or segfaults.
Incorrect (missing sentinel — reads past array bounds):
static ngx_command_t ngx_http_mymodule_commands[] = {
{ ngx_string("mymodule_enable"),
NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, enable),
NULL },
{ ngx_string("mymodule_timeout"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_msec_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, timeout),
NULL }
/* BUG: no ngx_null_command — parser reads past this array */
};Correct (sentinel terminates the array):
static ngx_command_t ngx_http_mymodule_commands[] = {
{ ngx_string("mymodule_enable"),
NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, enable),
NULL },
{ ngx_string("mymodule_timeout"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_msec_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, timeout),
NULL },
ngx_null_command
};Initialize Config Fields with UNSET Constants
nginx merges configuration from parent to child contexts (http -> server -> location). Fields initialized to 0 instead of NGX_CONF_UNSET cannot be distinguished from explicitly set values, so the merge step silently skips them and the parent value is never inherited.
Incorrect (zero-init prevents inheritance from parent block):
static void *
ngx_http_mymodule_create_loc_conf(ngx_conf_t *cf)
{
ngx_http_mymodule_loc_conf_t *conf;
conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_mymodule_loc_conf_t));
if (conf == NULL) {
return NULL;
}
/* BUG: 0 looks like an explicitly set value to the merge step */
conf->timeout = 0;
conf->enable = 0;
conf->max_retries = 0;
return conf;
}Correct (UNSET constants allow merge to detect unset fields):
static void *
ngx_http_mymodule_create_loc_conf(ngx_conf_t *cf)
{
ngx_http_mymodule_loc_conf_t *conf;
conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_mymodule_loc_conf_t));
if (conf == NULL) {
return NULL;
}
/* UNSET tells merge_loc_conf these fields need parent values */
conf->timeout = NGX_CONF_UNSET_MSEC;
conf->enable = NGX_CONF_UNSET;
conf->max_retries = NGX_CONF_UNSET_UINT;
return conf;
}Note: Use the type-appropriate constant: NGX_CONF_UNSET for ngx_flag_t and ngx_int_t, NGX_CONF_UNSET_UINT for ngx_uint_t, NGX_CONF_UNSET_MSEC for millisecond timers, NGX_CONF_UNSET_SIZE for size_t, and NGX_CONF_UNSET_PTR for pointers.
Use ngx_cpymem for Sequential Buffer Writes
ngx_cpymem returns a pointer to the byte immediately after the copied data. This enables clean sequential writes without manually computing offsets. Using ngx_memcpy instead requires error-prone offset arithmetic that silently produces overlapping writes or gaps when lengths are wrong.
Incorrect (manual pointer arithmetic with ngx_memcpy):
static ngx_int_t
ngx_http_mymodule_build_response(ngx_buf_t *b, ngx_str_t *prefix,
ngx_str_t *body, ngx_str_t *suffix)
{
u_char *p = b->pos;
ngx_memcpy(p, prefix->data, prefix->len);
/* BUG-PRONE: manual offset tracking — easy to get wrong */
ngx_memcpy(p + prefix->len, body->data, body->len);
ngx_memcpy(p + prefix->len + body->len, suffix->data, suffix->len);
b->last = p + prefix->len + body->len + suffix->len;
return NGX_OK;
}Correct (ngx_cpymem returns advanced pointer automatically):
static ngx_int_t
ngx_http_mymodule_build_response(ngx_buf_t *b, ngx_str_t *prefix,
ngx_str_t *body, ngx_str_t *suffix)
{
u_char *p = b->pos;
/* each call advances p past the copied bytes */
p = ngx_cpymem(p, prefix->data, prefix->len);
p = ngx_cpymem(p, body->data, body->len);
p = ngx_cpymem(p, suffix->data, suffix->len);
b->last = p;
return NGX_OK;
}Build Hash Tables During Configuration Only
ngx_hash_t is an immutable open-addressing hash table. Once built with ngx_hash_init, its bucket array is fixed and cannot accept new keys. Attempting to add entries at runtime corrupts the hash or triggers segfaults. All keys must be collected during configuration parsing and inserted before the hash is initialized.
Incorrect (attempts to add keys at request time):
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_http_mymodule_loc_conf_t *lcf;
ngx_hash_key_t key;
lcf = ngx_http_get_module_loc_conf(r, ngx_http_mymodule_module);
/* BUG: hash was already built in postconfiguration —
* there is no API to add keys after ngx_hash_init.
* ngx_hash_add_key operates on ngx_hash_keys_arrays_t during
* configuration, not on the finalized ngx_hash_t at runtime */
ngx_hash_add_key(&lcf->hosts_keys,
&r->headers_in.host->value,
(void *) 1,
NGX_HASH_READONLY_KEY);
return NGX_OK;
}Correct (builds hash during config, uses ngx_hash_find at runtime):
/* during configuration: collect keys and build hash */
static char *
ngx_http_mymodule_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
{
ngx_http_mymodule_loc_conf_t *conf = child;
ngx_hash_init_t hash;
hash.hash = &conf->hosts_hash;
hash.key = ngx_hash_key_lc;
hash.max_size = 512;
hash.bucket_size = 64;
hash.name = "mymodule_hosts";
hash.pool = cf->pool;
/* all keys must be in conf->hosts_keys before this call */
if (ngx_hash_init(&hash, conf->hosts_keys.elts,
conf->hosts_keys.nelts) != NGX_OK)
{
return NGX_CONF_ERROR;
}
return NGX_CONF_OK;
}
/* at runtime: read-only lookup */
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_http_mymodule_loc_conf_t *lcf;
void *val;
ngx_uint_t key;
lcf = ngx_http_get_module_loc_conf(r, ngx_http_mymodule_module);
if (r->headers_in.host == NULL) {
return NGX_DECLINED;
}
key = ngx_hash_key_lc(r->headers_in.host->value.data,
r->headers_in.host->value.len);
val = ngx_hash_find(&lcf->hosts_hash, key,
r->headers_in.host->value.data,
r->headers_in.host->value.len);
return (val != NULL) ? NGX_OK : NGX_DECLINED;
}Iterate ngx_list_t Using Part-Based Pattern
ngx_list_t is a linked list of arrays (parts), not individual elements. Each part holds up to nalloc elements. Iterating only the first part's elements misses all entries that overflowed into subsequent parts. The standard nginx iteration pattern tracks both the current part pointer and the element index, resetting the index when crossing part boundaries.
Incorrect (iterates only the first part, misses overflow elements):
static ngx_int_t
ngx_http_mymodule_scan_headers(ngx_http_request_t *r)
{
ngx_table_elt_t *h;
ngx_uint_t i;
h = r->headers_in.headers.part.elts;
/* BUG: only iterates first part — if headers overflow into
* additional parts, those headers are silently skipped */
for (i = 0; i < r->headers_in.headers.part.nelts; i++) {
ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
"header: %V = %V", &h[i].key, &h[i].value);
}
return NGX_OK;
}Correct (traverses all parts using standard nginx pattern):
static ngx_int_t
ngx_http_mymodule_scan_headers(ngx_http_request_t *r)
{
ngx_list_part_t *part;
ngx_table_elt_t *h;
ngx_uint_t i;
part = &r->headers_in.headers.part;
h = part->elts;
for (i = 0; /* void */; i++) {
if (i >= part->nelts) {
if (part->next == NULL) {
break; /* end of list */
}
part = part->next;
h = part->elts;
i = 0; /* reset index for new part */
}
ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
"header: %V = %V", &h[i].key, &h[i].value);
}
return NGX_OK;
}Never Assume ngx_str_t Is Null-Terminated
ngx_str_t stores a {len, data} pair without a null terminator. Passing str.data to standard C functions that expect null-terminated strings (printf %s, strcmp, atoi) reads past the buffer boundary, causing incorrect comparisons, garbled output, or segfaults.
Incorrect (passes ngx_str_t data to C string functions):
static ngx_int_t
ngx_http_mymodule_check_method(ngx_http_request_t *r)
{
/* BUG: %s reads until \0 — ngx_str_t has no terminator */
ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
"method: %s", r->method_name.data);
/* BUG: strcmp reads past buffer — undefined behavior */
if (strcmp((char *) r->method_name.data, "POST") == 0) {
return NGX_OK;
}
/* BUG: atoi reads unterminated string */
int val = atoi((char *) r->args.data);
return NGX_DECLINED;
}Correct (uses length-aware functions and format specifiers):
static ngx_int_t
ngx_http_mymodule_check_method(ngx_http_request_t *r)
{
/* %V is nginx's format specifier for ngx_str_t pointers */
ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
"method: %V", &r->method_name);
/* ngx_strncmp respects the string length */
if (r->method_name.len == 4
&& ngx_strncmp(r->method_name.data, "POST", 4) == 0)
{
return NGX_OK;
}
/* ngx_atoi takes explicit length parameter */
ngx_int_t val = ngx_atoi(r->args.data, r->args.len);
return NGX_DECLINED;
}Use ngx_string Macro Only with String Literals
The ngx_string() macro uses sizeof() - 1 at compile time to compute the string length. This only works with string literals where sizeof returns the array size. When used with a char * variable, sizeof returns the pointer size (4 or 8 bytes), producing a completely wrong length that causes truncated reads or buffer overflows.
Incorrect (ngx_string with pointer variable computes wrong length):
static ngx_int_t
ngx_http_mymodule_set_header(ngx_http_request_t *r, char *name)
{
ngx_str_t header_name;
/* BUG: sizeof(name) = sizeof(char*) = 8 on 64-bit
* so header_name.len = 7, regardless of actual string length */
header_name = ngx_string(name);
/* if name is "X-Request-ID" (12 chars), len is 7 — truncated */
ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
"header: %V", &header_name);
return NGX_OK;
}Correct (ngx_string for literals, manual assignment for variables):
static ngx_int_t
ngx_http_mymodule_set_header(ngx_http_request_t *r, char *name)
{
ngx_str_t header_name;
/* for variables: manually set data and compute length at runtime */
header_name.data = (u_char *) name;
header_name.len = ngx_strlen(name);
ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
"header: %V", &header_name);
return NGX_OK;
}
/* ngx_string is safe only with compile-time string literals */
static ngx_str_t default_type = ngx_string("application/octet-stream");
static ngx_str_t module_name = ngx_string("mymodule");Call ngx_handle_read/write_event After I/O Operations
After processing an I/O event, call ngx_handle_read_event or ngx_handle_write_event to re-register for notifications on that file descriptor. On edge-triggered systems (epoll with EPOLLET), missing this call means the connection permanently stalls -- no further events are delivered even when data becomes available.
Incorrect (processes data without re-registering for events):
static void
ngx_http_mymodule_read_handler(ngx_event_t *rev)
{
ngx_connection_t *c = rev->data;
ngx_int_t n;
n = c->recv(c, c->buffer->last,
c->buffer->end - c->buffer->last);
if (n == NGX_AGAIN) {
/* BUG: on epoll edge-triggered, no re-registration means
* this connection will never receive another read event */
return;
}
c->buffer->last += n;
ngx_http_mymodule_process(c);
/* BUG: no re-registration for next read */
}Correct (re-registers for events after I/O processing):
static void
ngx_http_mymodule_read_handler(ngx_event_t *rev)
{
ngx_connection_t *c = rev->data;
ngx_int_t n;
n = c->recv(c, c->buffer->last,
c->buffer->end - c->buffer->last);
if (n == NGX_AGAIN) {
if (ngx_handle_read_event(rev, 0) != NGX_OK) {
ngx_http_mymodule_finalize(c);
}
return;
}
c->buffer->last += n;
ngx_http_mymodule_process(c);
/* re-register for next read event */
if (ngx_handle_read_event(rev, 0) != NGX_OK) {
ngx_http_mymodule_finalize(c);
}
}Never Use Blocking Calls in Event Handlers
nginx workers use a single-threaded event loop that multiplexes thousands of connections. A blocking call -- connect(), sleep(), synchronous file I/O, or DNS resolution -- freezes the entire worker process, stalling every connection it manages until the blocking call completes or times out.
Incorrect (blocking connect and synchronous read in handler):
static void
ngx_http_mymodule_check_backend(ngx_event_t *ev)
{
int fd;
struct sockaddr sa;
char buf[1024];
/* BUG: blocking connect — freezes worker for up to TCP timeout */
fd = socket(AF_INET, SOCK_STREAM, 0);
connect(fd, &sa, sizeof(sa));
/* BUG: blocking read — worker stalls until data arrives */
read(fd, buf, sizeof(buf));
close(fd);
}Correct (non-blocking I/O through nginx event system):
static void
ngx_http_mymodule_check_backend(ngx_event_t *ev)
{
ngx_connection_t *c = ev->data;
ngx_int_t n;
/* non-blocking read via nginx connection abstraction */
n = c->recv(c, c->buffer->last,
c->buffer->end - c->buffer->last);
if (n == NGX_AGAIN) {
/* no data yet — re-register and return to event loop */
ngx_handle_read_event(c->read, 0);
return;
}
if (n == NGX_ERROR || n == 0) {
ngx_http_mymodule_finalize(c);
return;
}
c->buffer->last += n;
ngx_http_mymodule_process(c);
}Use Posted Events for Deferred Processing
Calling event handlers directly from within other handlers creates deep recursion that can overflow the stack, especially under high concurrency where chains of subrequest completions or cascading I/O triggers compound the depth. ngx_post_event defers execution to the next event loop iteration, flattening the call stack and making execution order predictable.
Incorrect (direct handler call creates deep recursion):
static void
ngx_http_mymodule_read_handler(ngx_event_t *rev)
{
ngx_connection_t *c = rev->data;
/* process incoming data */
ngx_http_mymodule_process(c);
/* BUG: direct call — if write_handler triggers another read,
* the call stack grows unbounded under load */
ngx_http_mymodule_write_handler(c->write);
}
static void
ngx_http_mymodule_write_handler(ngx_event_t *wev)
{
ngx_connection_t *c = wev->data;
ngx_http_mymodule_send(c);
/* BUG: recursive call back to read handler */
ngx_http_mymodule_read_handler(c->read);
}Correct (defers processing to next event loop iteration):
static void
ngx_http_mymodule_read_handler(ngx_event_t *rev)
{
ngx_connection_t *c = rev->data;
/* process incoming data */
ngx_http_mymodule_process(c);
/* defer write to next event loop iteration — flat call stack */
ngx_post_event(c->write, &ngx_posted_events);
}
static void
ngx_http_mymodule_write_handler(ngx_event_t *wev)
{
ngx_connection_t *c = wev->data;
ngx_http_mymodule_send(c);
/* defer read to next iteration instead of direct call */
ngx_post_event(c->read, &ngx_posted_events);
}Offload Blocking Operations to Thread Pool
CPU-intensive operations (cryptographic hashing, image transformation, compression) and unavoidable blocking calls (synchronous database queries, disk I/O on systems without AIO) block the event loop. Use ngx_thread_task_post to execute them in a thread pool, with a completion callback that runs back on the event loop thread to process results.
Incorrect (CPU-intensive work directly in event handler):
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
u_char hash[32];
u_char *data;
size_t len;
data = r->request_body->bufs->buf->pos;
len = r->request_body->bufs->buf->last - data;
/* BUG: blocks event loop for entire SHA-256 computation */
ngx_http_mymodule_sha256(data, len, hash);
return ngx_http_mymodule_send_response(r, hash, 32);
}Correct (offloads work to thread pool with completion callback):
typedef struct {
ngx_http_request_t *request;
u_char *data;
size_t len;
u_char hash[32];
} ngx_http_mymodule_ctx_t;
/* runs in thread pool — must NOT access r or any nginx structures */
static void
ngx_http_mymodule_sha256_thread(void *data, ngx_log_t *log)
{
ngx_http_mymodule_ctx_t *ctx = data;
/* safe: only accesses data copied into ctx before posting */
ngx_http_mymodule_sha256(ctx->data, ctx->len, ctx->hash);
}
/* runs on event loop thread — safe to access request and finalize */
static void
ngx_http_mymodule_sha256_done(ngx_event_t *ev)
{
ngx_http_mymodule_ctx_t *ctx;
ngx_http_request_t *r;
ctx = ev->data;
r = ctx->request;
/* finalize decrements r->main->count internally */
ngx_http_finalize_request(r,
ngx_http_mymodule_send_response(r, ctx->hash, 32));
}
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_thread_task_t *task;
ngx_http_mymodule_ctx_t *ctx;
ngx_thread_pool_t *tp;
tp = ngx_thread_pool_get((ngx_cycle_t *) ngx_cycle, &pool_name);
task = ngx_thread_task_alloc(r->pool, sizeof(ngx_http_mymodule_ctx_t));
if (task == NULL) {
return NGX_ERROR;
}
ctx = task->ctx;
ctx->request = r;
ctx->data = r->request_body->bufs->buf->pos;
ctx->len = r->request_body->bufs->buf->last - ctx->data;
task->handler = ngx_http_mymodule_sha256_thread; /* runs in thread */
task->event.handler = ngx_http_mymodule_sha256_done; /* completion */
task->event.data = ctx;
/* keep request alive while thread work is pending */
r->main->count++;
return ngx_thread_task_post(tp, task);
}Note: The thread handler must NOT access r or any nginx structures directly — only data copied into the task context before posting. The completion callback runs on the event loop thread where it is safe to call ngx_http_finalize_request. Set task->event.data to the context (not r) so the completion handler can access both the results and the request.
Delete Timers Before Freeing Associated Data
Timer events are stored in a global red-black tree. If the data structure associated with a timer is freed while the timer remains registered, the timer eventually fires and dereferences freed memory -- causing segfaults, corruption, or silent data mangling. Always call ngx_del_timer before destroying the timer's context.
Incorrect (frees connection without deleting pending timer):
static void
ngx_http_mymodule_close(ngx_connection_t *c)
{
ngx_http_mymodule_ctx_t *ctx = c->data;
/* BUG: timer still in rbtree — will fire and access freed ctx */
ngx_pfree(c->pool, ctx);
ngx_close_connection(c);
}Correct (deletes timer before freeing associated data):
static void
ngx_http_mymodule_close(ngx_connection_t *c)
{
ngx_http_mymodule_ctx_t *ctx = c->data;
/* remove timer from rbtree before freeing its context */
if (c->read->timer_set) {
ngx_del_timer(c->read);
}
if (c->write->timer_set) {
ngx_del_timer(c->write);
}
ngx_pfree(c->pool, ctx);
ngx_close_connection(c);
}Iterate Buffer Chains Using cl->next Pattern
Response bodies arrive as linked lists of ngx_chain_t nodes. Each node holds a ngx_buf_t pointer and a next link. Processing only the first buffer silently discards the rest of the response, producing truncated output for any response larger than a single buffer.
Incorrect (processes only the first buffer in the chain):
static ngx_int_t
ngx_http_myfilter_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
u_char *p;
size_t len;
if (in == NULL) {
return ngx_http_next_body_filter(r, in);
}
/* BUG: only processes in->buf — remaining buffers are ignored */
p = in->buf->pos;
len = in->buf->last - in->buf->pos;
ngx_http_myfilter_transform(p, len);
return ngx_http_next_body_filter(r, in);
}Correct (iterates entire buffer chain):
static ngx_int_t
ngx_http_myfilter_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
ngx_chain_t *cl;
u_char *p;
size_t len;
for (cl = in; cl; cl = cl->next) {
if (cl->buf->in_file) {
continue; /* skip file buffers if only processing memory */
}
p = cl->buf->pos;
len = cl->buf->last - cl->buf->pos;
if (len > 0) {
ngx_http_myfilter_transform(p, len);
}
}
return ngx_http_next_body_filter(r, in);
}Set Buffering Flag When Accumulating Response Data
When a filter needs to accumulate data before passing it downstream (e.g., for gzip, substitution, or content rewriting), it must set r->buffered |= NGX_HTTP_MY_MODULE to signal nginx that output is being held. Without this flag, nginx may flush the response prematurely or report errors when the expected content length does not match bytes sent.
Incorrect (holds data without setting buffered flag):
#define NGX_HTTP_MYFILTER_BUFFERED 0x08
static ngx_int_t
ngx_http_myfilter_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
ngx_http_myfilter_ctx_t *ctx;
ctx = ngx_http_get_module_ctx(r, ngx_http_myfilter_module);
/* accumulate buffers for later processing */
if (ngx_chain_add_copy(r->pool, &ctx->pending, in) != NGX_OK) {
return NGX_ERROR;
}
/* BUG: nginx thinks output is complete — may send incomplete response */
return NGX_OK;
}Correct (sets buffered flag while holding data, clears on flush):
#define NGX_HTTP_MYFILTER_BUFFERED 0x08
static ngx_int_t
ngx_http_myfilter_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
ngx_http_myfilter_ctx_t *ctx;
ctx = ngx_http_get_module_ctx(r, ngx_http_myfilter_module);
if (ngx_chain_add_copy(r->pool, &ctx->pending, in) != NGX_OK) {
return NGX_ERROR;
}
if (!ctx->ready_to_flush) {
/* signal nginx: this module is holding buffered data */
r->buffered |= NGX_HTTP_MYFILTER_BUFFERED;
return NGX_OK;
}
/* ready to send — clear flag and flush accumulated data */
r->buffered &= ~NGX_HTTP_MYFILTER_BUFFERED;
return ngx_http_next_body_filter(r, ctx->pending);
}Always Call Next Filter in the Chain
Every filter MUST call ngx_http_next_header_filter or ngx_http_next_body_filter to pass control downstream. Returning NGX_OK without calling the next filter silently drops the response body, causing the client to hang waiting for data that will never arrive.
Incorrect (returns without calling next body filter):
static ngx_int_t
ngx_http_myfilter_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
ngx_http_myfilter_ctx_t *ctx;
ctx = ngx_http_get_module_ctx(r, ngx_http_myfilter_module);
if (ctx == NULL) {
/* BUG: response is silently dropped — client hangs indefinitely */
return NGX_OK;
}
/* process buffers... */
return ngx_http_next_body_filter(r, in);
}Correct (always calls next filter, even on early return):
static ngx_int_t
ngx_http_myfilter_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
ngx_http_myfilter_ctx_t *ctx;
ctx = ngx_http_get_module_ctx(r, ngx_http_myfilter_module);
if (ctx == NULL) {
/* pass through — let downstream filters handle it */
return ngx_http_next_body_filter(r, in);
}
/* process buffers... */
return ngx_http_next_body_filter(r, in);
}Distinguish Main Request from Subrequest in Filters
Filters execute for ALL requests including subrequests (SSI includes, auth subrequests, etc.). Applying transformations unconditionally corrupts subrequest responses or double-processes content. Always check r == r->main to determine if the current request is the main client request.
Incorrect (modifies headers for every request including subrequests):
static ngx_int_t
ngx_http_myfilter_header_filter(ngx_http_request_t *r)
{
/* BUG: overwrites content-type on auth subrequests and SSI includes */
r->headers_out.content_type_len = sizeof("text/html") - 1;
ngx_str_set(&r->headers_out.content_type, "text/html");
r->headers_out.content_length_n = -1;
ngx_http_clear_content_length(r);
return ngx_http_next_header_filter(r);
}Correct (only transforms main request responses):
static ngx_int_t
ngx_http_myfilter_header_filter(ngx_http_request_t *r)
{
/* skip subrequests — only transform the main client response */
if (r != r->main) {
return ngx_http_next_header_filter(r);
}
r->headers_out.content_type_len = sizeof("text/html") - 1;
ngx_str_set(&r->headers_out.content_type, "text/html");
r->headers_out.content_length_n = -1;
ngx_http_clear_content_length(r);
return ngx_http_next_header_filter(r);
}Save and Replace Top Filter in postconfiguration
Filters form a singly-linked list built at startup. Each filter saves the current top filter pointer as its "next" and installs itself as the new top. Forgetting to save the current top before overwriting it breaks the chain, causing downstream filters to be silently skipped or creating infinite loops.
Incorrect (overwrites top filter without saving next):
static ngx_int_t
ngx_http_myfilter_init(ngx_conf_t *cf)
{
/* BUG: previous top filter is lost — all downstream filters skipped */
ngx_http_top_body_filter = ngx_http_myfilter_body_filter;
return NGX_OK;
}
static ngx_int_t
ngx_http_myfilter_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
/* process buffers... */
/* BUG: ngx_http_next_body_filter was never set — crashes or hangs */
return ngx_http_next_body_filter(r, in);
}Correct (save current top as next, then install self):
static ngx_http_output_header_filter_pt ngx_http_next_header_filter;
static ngx_http_output_body_filter_pt ngx_http_next_body_filter;
static ngx_int_t
ngx_http_myfilter_init(ngx_conf_t *cf)
{
/* save current top so we can call it as "next" */
ngx_http_next_header_filter = ngx_http_top_header_filter;
ngx_http_top_header_filter = ngx_http_myfilter_header_filter;
ngx_http_next_body_filter = ngx_http_top_body_filter;
ngx_http_top_body_filter = ngx_http_myfilter_body_filter;
return NGX_OK;
}Register Custom Variables in preconfiguration
Custom nginx variables (accessible as $my_variable in configuration) must be registered during the preconfiguration callback using ngx_http_add_variable. Registration in postconfiguration or later is too late — the variable hash has already been built and config parsing has already resolved variable references. A variable registered too late silently does not exist, causing config errors when referenced.
Incorrect (registers variable in postconfiguration — too late):
static ngx_int_t
ngx_http_mymodule_postconfiguration(ngx_conf_t *cf)
{
ngx_http_variable_t *var;
/* BUG: variable hash is built from preconfiguration registrations —
* this variable will not be found during config parsing */
var = ngx_http_add_variable(cf, &ngx_http_mymodule_var_name,
NGX_HTTP_VAR_NOCACHEABLE);
if (var == NULL) {
return NGX_ERROR;
}
var->get_handler = ngx_http_mymodule_var_handler;
return NGX_OK;
}Correct (registers variable in preconfiguration with proper flags):
static ngx_str_t ngx_http_mymodule_var_name =
ngx_string("mymodule_request_id");
static ngx_int_t
ngx_http_mymodule_var_handler(ngx_http_request_t *r,
ngx_http_variable_value_t *v, uintptr_t data)
{
my_ctx_t *ctx;
ctx = ngx_http_get_module_ctx(r, ngx_http_mymodule_module);
if (ctx == NULL || ctx->request_id.len == 0) {
v->not_found = 1;
return NGX_OK;
}
v->data = ctx->request_id.data;
v->len = ctx->request_id.len;
v->valid = 1;
v->no_cacheable = 0;
v->not_found = 0;
return NGX_OK;
}
static ngx_int_t
ngx_http_mymodule_preconfiguration(ngx_conf_t *cf)
{
ngx_http_variable_t *var;
var = ngx_http_add_variable(cf, &ngx_http_mymodule_var_name,
NGX_HTTP_VAR_NOCACHEABLE);
if (var == NULL) {
return NGX_ERROR;
}
var->get_handler = ngx_http_mymodule_var_handler;
return NGX_OK;
}Note: Use NGX_HTTP_VAR_NOCACHEABLE when the variable value changes per request (e.g., computed from request data). Use NGX_HTTP_VAR_CHANGEABLE if other modules should be allowed to redefine the variable. The get_handler is called lazily — only when the variable is actually referenced — so expensive computation is deferred until needed.
Reference: nginx Development Guide — Variables
Use content_handler for Location-Specific Response Generation
For modules that generate the complete response for a specific location, set the location's handler field directly from the directive setter. This bypasses the phase handler iteration entirely, which avoids executing unrelated phase handlers and provides a clean ownership model -- one location, one content generator.
Incorrect (phase handler runs for every request, not just the target location):
static ngx_int_t
ngx_http_mymodule_postconfiguration(ngx_conf_t *cf)
{
ngx_http_core_main_conf_t *cmcf;
ngx_http_handler_pt *h;
cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);
/* runs for ALL requests — must check location config on each call */
h = ngx_array_push(&cmcf->phases[NGX_HTTP_CONTENT_PHASE].handlers);
if (h == NULL) {
return NGX_ERROR;
}
*h = ngx_http_mymodule_handler;
return NGX_OK;
}Correct (content handler set per-location in the directive setter):
static char *
ngx_http_mymodule_set(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);
/* handler runs ONLY for requests matching this location */
clcf->handler = ngx_http_mymodule_handler;
return NGX_CONF_OK;
}Note: This pattern is used by standard modules like ngx_http_proxy_module and ngx_http_static_module. Phase handlers are better suited for cross-cutting concerns (access control, logging) that should run regardless of which location is matched.
Use header_only for Empty Body Responses
For responses with no body -- such as 204 No Content, 304 Not Modified, or 3xx redirects -- set content_length_n = 0 and return the result of ngx_http_send_header directly. There is no need to allocate a buffer or chain. Creating empty buffers wastes pool memory and adds unnecessary complexity to the output filter chain.
Incorrect (allocating an empty buffer for a 204 response):
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_buf_t *b;
ngx_chain_t out;
r->headers_out.status = NGX_HTTP_NO_CONTENT;
r->headers_out.content_length_n = 0;
ngx_http_send_header(r);
/* BUG: unnecessary allocation — 204 has no body */
b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
if (b == NULL) {
return NGX_ERROR;
}
b->last_buf = 1;
out.buf = b;
out.next = NULL;
return ngx_http_output_filter(r, &out);
}Correct (header-only response with no buffer allocation):
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
r->headers_out.status = NGX_HTTP_NO_CONTENT;
r->headers_out.content_length_n = 0;
r->header_only = 1;
return ngx_http_send_header(r);
}Note: The r->header_only flag tells the downstream filter chain that no body will follow. This also works for redirect responses where you set headers_out.location and return the header with a 301/302 status.
Return HTTP Status Codes for Error Responses
Returning NGX_HTTP_* status codes (e.g., NGX_HTTP_FORBIDDEN, NGX_HTTP_NOT_FOUND) from a handler lets nginx's error_page directive intercept the error and serve a custom response. Manually constructing an error body and sending it directly bypasses this mechanism, preventing administrators from customizing error pages in configuration.
Incorrect (manually building error response bypasses error_page):
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_buf_t *b;
ngx_chain_t out;
if (!ngx_http_mymodule_check_auth(r)) {
/* BUG: sends hardcoded body — error_page directive is bypassed */
r->headers_out.status = NGX_HTTP_FORBIDDEN;
r->headers_out.content_length_n = 9;
ngx_str_set(&r->headers_out.content_type, "text/plain");
ngx_http_send_header(r);
b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
b->pos = (u_char *) "Forbidden";
b->last = b->pos + 9;
b->memory = 1;
b->last_buf = 1;
out.buf = b;
out.next = NULL;
return ngx_http_output_filter(r, &out);
}
return NGX_DECLINED;
}Correct (return status code and let nginx handle error pages):
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
if (!ngx_http_mymodule_check_auth(r)) {
/* nginx will use error_page 403 if configured */
return NGX_HTTP_FORBIDDEN;
}
return NGX_DECLINED;
}Note: This applies to access phase handlers and content handlers that need to reject requests. Only build custom response bodies when you intentionally want to override the error_page mechanism with module-specific output.
Set last_buf Flag on Final Buffer
The last_buf = 1 flag signals the end of a response to the downstream filter chain. Without it, nginx keeps the connection open expecting more output buffers, causing the client to hang until the connection times out. For subrequests, use last_in_chain = 1 instead, since only the main request owns the connection.
Incorrect (missing last_buf — client hangs waiting for more data):
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_buf_t *b;
ngx_chain_t out;
ngx_int_t rc;
r->headers_out.status = NGX_HTTP_OK;
r->headers_out.content_length_n = 11;
ngx_str_set(&r->headers_out.content_type, "text/plain");
rc = ngx_http_send_header(r);
if (rc == NGX_ERROR || rc > NGX_OK) {
return rc;
}
b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
if (b == NULL) {
return NGX_ERROR;
}
b->pos = (u_char *) "Hello World";
b->last = b->pos + 11;
b->memory = 1;
/* BUG: last_buf not set — nginx waits for more buffers */
out.buf = b;
out.next = NULL;
return ngx_http_output_filter(r, &out);
}Correct (last_buf set on final buffer, last_in_chain for subrequests):
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_buf_t *b;
ngx_chain_t out;
ngx_int_t rc;
r->headers_out.status = NGX_HTTP_OK;
r->headers_out.content_length_n = 11;
ngx_str_set(&r->headers_out.content_type, "text/plain");
rc = ngx_http_send_header(r);
if (rc == NGX_ERROR || rc > NGX_OK) {
return rc;
}
b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
if (b == NULL) {
return NGX_ERROR;
}
b->pos = (u_char *) "Hello World";
b->last = b->pos + 11;
b->memory = 1;
if (r == r->main) {
b->last_buf = 1; /* main request: signals end of response */
} else {
b->last_in_chain = 1; /* subrequest: signals end of this chain */
}
out.buf = b;
out.next = NULL;
return ngx_http_output_filter(r, &out);
}Use Module Context for Per-Request State
Each request needs its own isolated state. ngx_http_set_ctx associates a module-specific context with a request, and ngx_http_get_module_ctx retrieves it. Using global variables or static data for per-request state causes concurrent requests to corrupt each other's data, since a single worker handles thousands of requests interleaved by the event loop.
Incorrect (static variable shared across all concurrent requests):
/* BUG: single instance shared by ALL requests in this worker */
static my_ctx_t global_ctx;
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
/* BUG: request A's data is overwritten when request B arrives */
global_ctx.uri = r->uri;
global_ctx.state = STATE_INIT;
return ngx_http_mymodule_process(r, &global_ctx);
}Correct (per-request context allocated from request pool):
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
my_ctx_t *ctx;
/* check if context already exists (e.g., re-entry after async) */
ctx = ngx_http_get_module_ctx(r, ngx_http_mymodule_module);
if (ctx == NULL) {
/* first call — allocate and initialize context */
ctx = ngx_pcalloc(r->pool, sizeof(my_ctx_t));
if (ctx == NULL) {
return NGX_ERROR;
}
ctx->state = STATE_INIT;
/* bind context to this request for this module */
ngx_http_set_ctx(r, ctx, ngx_http_mymodule_module);
}
/* ctx is isolated per-request — safe with concurrent requests */
return ngx_http_mymodule_process(r, ctx);
}Note: The context is automatically freed when r->pool is destroyed at request end. Always use ngx_pcalloc to zero-initialize the context so all pointers start as NULL and all integers as 0. The get_module_ctx / set_ctx pair is used in virtually every nginx module — filters, access handlers, and upstream modules all follow this pattern.
Reference: nginx Development Guide — HTTP Request
Register Phase Handlers in postconfiguration
Phase handlers must be registered during the postconfiguration callback in ngx_http_module_t. This is the only point in the startup sequence where the phases array is being built. Adding handlers in init_module or init_process is too late -- the phase engine has already been compiled into the phase handlers array, and new entries are ignored.
Incorrect (registering handler in init_process -- too late):
static ngx_int_t
ngx_http_mymodule_init_process(ngx_cycle_t *cycle)
{
ngx_http_core_main_conf_t *cmcf;
ngx_http_handler_pt *h;
cmcf = ngx_http_cycle_get_module_main_conf(cycle,
ngx_http_core_module);
/* BUG: phase engine is already built at this point —
* handler is never called */
h = ngx_array_push(&cmcf->phases[NGX_HTTP_ACCESS_PHASE].handlers);
if (h == NULL) {
return NGX_ERROR;
}
*h = ngx_http_mymodule_access_handler;
return NGX_OK;
}Correct (registering handler in postconfiguration):
static ngx_int_t
ngx_http_mymodule_postconfiguration(ngx_conf_t *cf)
{
ngx_http_core_main_conf_t *cmcf;
ngx_http_handler_pt *h;
cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);
h = ngx_array_push(&cmcf->phases[NGX_HTTP_ACCESS_PHASE].handlers);
if (h == NULL) {
return NGX_ERROR;
}
*h = ngx_http_mymodule_access_handler;
return NGX_OK;
}Note: Not all phases accept handlers. NGX_HTTP_FIND_CONFIG_PHASE, NGX_HTTP_POST_REWRITE_PHASE, and NGX_HTTP_POST_ACCESS_PHASE are internal and cannot have user-registered handlers.
Send Header Before Body Output
ngx_http_send_header must be called before ngx_http_output_filter. The header call writes the HTTP status line and headers to the client, and the output filter sends the body. Reversing the order or skipping the header call produces malformed HTTP that confuses clients, breaks keep-alive connections, and causes proxy chaining failures.
Incorrect (body sent without prior header call):
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_buf_t *b;
ngx_chain_t out;
b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
if (b == NULL) {
return NGX_ERROR;
}
b->pos = (u_char *) "Hello";
b->last = b->pos + 5;
b->memory = 1;
b->last_buf = 1;
out.buf = b;
out.next = NULL;
/* BUG: no ngx_http_send_header — client receives body
* without status line or headers */
return ngx_http_output_filter(r, &out);
}Correct (headers set and sent before body output):
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_int_t rc;
ngx_buf_t *b;
ngx_chain_t out;
r->headers_out.status = NGX_HTTP_OK;
r->headers_out.content_length_n = 5;
ngx_str_set(&r->headers_out.content_type, "text/plain");
rc = ngx_http_send_header(r);
if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) {
return rc;
}
b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
if (b == NULL) {
return NGX_ERROR;
}
b->pos = (u_char *) "Hello";
b->last = b->pos + 5;
b->memory = 1;
b->last_buf = 1;
out.buf = b;
out.next = NULL;
return ngx_http_output_filter(r, &out);
}Check Every Allocation Return for NULL
ngx_palloc, ngx_pcalloc, and ngx_pnalloc all return NULL when the pool cannot satisfy the allocation. Dereferencing a NULL pointer crashes the worker process, taking down all connections it serves.
Incorrect (missing NULL check causes segfault):
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_buf_t *b;
ngx_chain_t *out;
b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
/* BUG: no NULL check — segfault if pool exhausted */
b->pos = ngx_palloc(r->pool, 256);
b->last = b->pos + 256;
out = ngx_palloc(r->pool, sizeof(ngx_chain_t));
out->buf = b;
out->next = NULL;
return ngx_http_output_filter(r, out);
}Correct (NULL check on every allocation):
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_buf_t *b;
ngx_chain_t *out;
b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
if (b == NULL) {
return NGX_ERROR;
}
b->pos = ngx_palloc(r->pool, 256);
if (b->pos == NULL) {
return NGX_ERROR;
}
b->last = b->pos + 256;
out = ngx_palloc(r->pool, sizeof(ngx_chain_t));
if (out == NULL) {
return NGX_ERROR;
}
out->buf = b;
out->next = NULL;
return ngx_http_output_filter(r, out);
}Note: Returning NGX_ERROR from a handler triggers request finalization, which destroys r->pool and frees all prior allocations. There is no need to manually clean up earlier successful allocations.
Register Pool Cleanup Handlers for External Resources
Pool destruction only frees memory allocated from the pool itself. External resources such as file descriptors, library handles, shared memory mappings, and network sockets require explicit cleanup via ngx_pool_cleanup_add. Without a cleanup handler, error paths and abnormal request termination leak these resources, eventually exhausting system limits.
Incorrect (file descriptor leaks on error paths):
static ngx_int_t
ngx_http_mymodule_open_db(ngx_http_request_t *r, my_ctx_t *ctx)
{
ctx->db_fd = open("/var/lib/mymodule/data.db", O_RDONLY);
if (ctx->db_fd == -1) {
ngx_log_error(NGX_LOG_ERR, r->connection->log, ngx_errno,
"failed to open database");
return NGX_ERROR;
}
/* BUG: if request finishes or errors out, db_fd is never closed */
/* pool destruction frees ctx memory but does not close the fd */
return NGX_OK;
}Correct (cleanup handler closes resource on pool destruction):
static void
ngx_http_mymodule_cleanup_db(void *data)
{
my_ctx_t *ctx = data;
if (ctx->db_fd != -1) {
close(ctx->db_fd);
ctx->db_fd = -1;
}
}
static ngx_int_t
ngx_http_mymodule_open_db(ngx_http_request_t *r, my_ctx_t *ctx)
{
ngx_pool_cleanup_t *cln;
cln = ngx_pool_cleanup_add(r->pool, 0);
if (cln == NULL) {
return NGX_ERROR;
}
/* initialize sentinel BEFORE registering cleanup — ngx_pcalloc zeros
* to 0 which is a valid fd on Unix, not our sentinel -1 */
ctx->db_fd = -1;
cln->handler = ngx_http_mymodule_cleanup_db;
cln->data = ctx;
ctx->db_fd = open("/var/lib/mymodule/data.db", O_RDONLY);
if (ctx->db_fd == -1) {
ngx_log_error(NGX_LOG_ERR, r->connection->log, ngx_errno,
"failed to open database");
return NGX_ERROR;
}
return NGX_OK;
}Note: Initialize the sentinel value (ctx->db_fd = -1) explicitly before registering the cleanup handler. ngx_pcalloc zeros memory to 0, which is a valid file descriptor on Unix — not a safe sentinel. Register the cleanup handler before open() so that if the pool is destroyed between the open and the return, the fd is still closed. This pattern is used throughout nginx core for temporary files (ngx_pool_cleanup_file).
Use ngx_pcalloc for Struct Initialization
ngx_pcalloc zeros all memory before returning, ensuring every struct field starts at a known state. Using ngx_palloc with manual field assignment risks leaving fields uninitialized, which causes undefined behavior when those fields are later read by nginx internals or other modules.
Incorrect (ngx_palloc with incomplete field assignment):
static ngx_int_t
ngx_http_mymodule_create_ctx(ngx_http_request_t *r)
{
my_ctx_t *ctx;
/* ngx_palloc does NOT zero memory */
ctx = ngx_palloc(r->pool, sizeof(my_ctx_t));
if (ctx == NULL) {
return NGX_ERROR;
}
ctx->state = STATE_INIT;
ctx->count = 0;
/* BUG: ctx->chain, ctx->upstream, ctx->flags left uninitialized */
/* reading ctx->chain later produces garbage pointer — crash or corruption */
ngx_http_set_ctx(r, ctx, ngx_http_mymodule_module);
return NGX_OK;
}Correct (ngx_pcalloc zeros all fields, then set non-zero values):
static ngx_int_t
ngx_http_mymodule_create_ctx(ngx_http_request_t *r)
{
my_ctx_t *ctx;
/* ngx_pcalloc zeros all memory — all pointers NULL, all ints 0 */
ctx = ngx_pcalloc(r->pool, sizeof(my_ctx_t));
if (ctx == NULL) {
return NGX_ERROR;
}
/* only set fields that need non-zero values */
ctx->state = STATE_INIT;
/* ctx->chain == NULL, ctx->upstream == NULL, ctx->flags == 0 */
ngx_http_set_ctx(r, ctx, ngx_http_mymodule_module);
return NGX_OK;
}Note: This is especially important for structs containing ngx_chain_t *, ngx_buf_t *, or callback function pointers. Uninitialized pointers are the leading cause of hard-to-reproduce worker crashes.
Avoid Relying on ngx_pfree for Pool Allocations
ngx_pfree only frees allocations larger than NGX_MAX_ALLOC_FROM_POOL (typically ~4KB). For small allocations, which represent the vast majority of pool usage, ngx_pfree is a silent no-op that returns NGX_DECLINED. Calling it on small allocations gives a false sense of memory recovery while the memory remains allocated until the entire pool is destroyed.
Incorrect (assuming ngx_pfree reclaims small allocations):
static ngx_int_t
ngx_http_mymodule_process(ngx_http_request_t *r)
{
u_char *scratch;
size_t len = 512;
scratch = ngx_palloc(r->pool, len);
if (scratch == NULL) {
return NGX_ERROR;
}
/* ... use scratch for intermediate header processing ... */
/* BUG: this is a no-op — 512 < NGX_MAX_ALLOC_FROM_POOL */
/* memory is NOT reclaimed; pool still holds 512 bytes */
ngx_pfree(r->pool, scratch);
/* allocating again thinking previous memory was freed */
scratch = ngx_palloc(r->pool, len);
/* pool now holds 1024 bytes, not 512 */
return NGX_OK;
}Correct (design allocation patterns for pool lifetime):
static ngx_int_t
ngx_http_mymodule_process(ngx_http_request_t *r)
{
u_char *buf;
size_t len = 512;
/*
* Accept that small allocations live until pool destruction.
* Allocate once and reuse the buffer for multiple operations.
*/
buf = ngx_palloc(r->pool, len);
if (buf == NULL) {
return NGX_ERROR;
}
/* phase 1: use buf for header serialization */
ngx_memcpy(buf, header_value.data, header_value.len);
/* phase 2: reuse same buf for body prefix */
ngx_memzero(buf, len);
ngx_memcpy(buf, body_prefix.data, body_prefix.len);
return NGX_OK;
}Note: If you genuinely need to free and reallocate repeatedly during a request, consider creating a child pool with ngx_create_pool() that you destroy and recreate between phases. This is how nginx handles subrequest isolation internally.
Use ngx_pnalloc for String Data Allocation
ngx_palloc aligns allocations to the platform pointer size (typically 8 bytes on 64-bit). String buffers (u_char *) do not require alignment, so ngx_pnalloc skips the alignment step. This follows the nginx core convention — ngx_pstrdup() uses ngx_pnalloc internally — and avoids unnecessary alignment padding on every string allocation.
Incorrect (aligned allocation for string data wastes memory):
static ngx_int_t
ngx_http_mymodule_set_header(ngx_http_request_t *r, ngx_str_t *value)
{
u_char *p;
/* ngx_palloc aligns to NGX_ALIGNMENT — wasteful for byte arrays */
p = ngx_palloc(r->pool, value->len + 1);
if (p == NULL) {
return NGX_ERROR;
}
ngx_memcpy(p, value->data, value->len);
p[value->len] = '\0';
return NGX_OK;
}Correct (unaligned allocation for string data):
static ngx_int_t
ngx_http_mymodule_set_header(ngx_http_request_t *r, ngx_str_t *value)
{
u_char *p;
/* ngx_pnalloc skips alignment — correct for u_char buffers */
p = ngx_pnalloc(r->pool, value->len + 1);
if (p == NULL) {
return NGX_ERROR;
}
ngx_memcpy(p, value->data, value->len);
p[value->len] = '\0';
return NGX_OK;
}Note: Use ngx_palloc when allocating structs, pointers, or any type requiring natural alignment. Use ngx_pnalloc exclusively for u_char string buffers. The nginx core follows this convention consistently — see ngx_pstrdup() which uses ngx_pnalloc internally.
Use Pool Allocation Instead of Heap malloc
nginx pools automatically free all allocations when the pool is destroyed at request end or connection close. Using ngx_alloc() (a malloc wrapper) introduces manual memory management that inevitably leaks in error paths, since early returns skip cleanup code.
Incorrect (heap allocation leaks on error paths):
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
my_ctx_t *ctx;
/* ngx_alloc wraps malloc — caller must ngx_free manually */
ctx = ngx_alloc(sizeof(my_ctx_t), r->connection->log);
if (ctx == NULL) {
return NGX_ERROR;
}
ctx->buf = ngx_alloc(4096, r->connection->log);
if (ctx->buf == NULL) {
/* BUG: forgot ngx_free(ctx) — leaked on this error path */
return NGX_ERROR;
}
/* every return path must call ngx_free(ctx->buf) + ngx_free(ctx) */
return NGX_OK;
}Correct (pool allocation auto-frees with request):
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
my_ctx_t *ctx;
/* pool allocation — freed automatically when r->pool is destroyed */
ctx = ngx_palloc(r->pool, sizeof(my_ctx_t));
if (ctx == NULL) {
return NGX_ERROR;
}
ctx->buf = ngx_palloc(r->pool, 4096);
if (ctx->buf == NULL) {
/* no leak — ctx is freed when request pool is destroyed */
return NGX_ERROR;
}
return NGX_OK;
}Note: Use r->pool for request-scoped data, cf->pool for configuration-lifetime data, and c->pool for connection-scoped data. Match the pool lifetime to the data lifetime.
Use Slab Allocator for Shared Memory Zones
Worker processes share memory zones for cross-process state such as rate counters, caches, and session stores. All access to shared memory must use ngx_slab_alloc for allocation and be protected by the zone's built-in mutex. Missing locks cause race conditions where concurrent workers corrupt shared data structures, producing intermittent and hard-to-diagnose failures.
Incorrect (unprotected shared memory access):
static ngx_int_t
ngx_http_mymodule_increment(ngx_http_request_t *r, ngx_shm_zone_t *zone)
{
my_shm_data_t *data;
ngx_slab_pool_t *shpool;
shpool = (ngx_slab_pool_t *) zone->shm.addr;
data = (my_shm_data_t *) shpool->data;
/* BUG: no mutex — two workers can read-modify-write simultaneously */
data->counter++;
/* BUG: ngx_slab_alloc without lock — corrupts slab free lists */
data->entry = ngx_slab_alloc(shpool, sizeof(my_entry_t));
return NGX_OK;
}Correct (mutex-protected shared memory access with slab allocation):
static ngx_int_t
ngx_http_mymodule_increment(ngx_http_request_t *r, ngx_shm_zone_t *zone)
{
my_shm_data_t *data;
my_entry_t *entry;
ngx_slab_pool_t *shpool;
shpool = (ngx_slab_pool_t *) zone->shm.addr;
ngx_shmtx_lock(&shpool->mutex);
data = (my_shm_data_t *) shpool->data;
data->counter++;
entry = ngx_slab_alloc_locked(shpool, sizeof(my_entry_t));
if (entry == NULL) {
ngx_shmtx_unlock(&shpool->mutex);
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
"slab alloc failed in shared zone");
return NGX_ERROR;
}
data->entry = entry;
ngx_shmtx_unlock(&shpool->mutex);
return NGX_OK;
}Note: Use ngx_slab_alloc_locked when you already hold the mutex to avoid a deadlock on re-locking. Use ngx_slab_alloc (which locks internally) only when you are not holding the mutex. Always minimize the critical section to reduce lock contention across workers.
Handle Request Body Reading Asynchronously
ngx_http_read_client_request_body is asynchronous -- the post_handler callback fires when the body is fully read. The handler must return NGX_DONE after initiating the read, never access r->request_body->bufs inline, and perform all body processing inside the callback.
Incorrect (reading body synchronously):
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_int_t rc;
rc = ngx_http_read_client_request_body(r, ngx_http_mymodule_body_handler);
/* BUG: body may not be available yet — read is async */
if (r->request_body && r->request_body->bufs) {
ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
"body length: %uz", r->request_body->bufs->buf->last
- r->request_body->bufs->buf->pos);
}
return rc;
}Correct (process body in callback, return NGX_DONE):
static void
ngx_http_mymodule_body_handler(ngx_http_request_t *r)
{
/* body is now fully read — safe to access bufs */
if (r->request_body == NULL || r->request_body->bufs == NULL) {
ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
return;
}
/* process body here, then finalize */
ngx_http_finalize_request(r, ngx_http_mymodule_process_body(r));
}
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_int_t rc;
rc = ngx_http_read_client_request_body(r, ngx_http_mymodule_body_handler);
if (rc >= NGX_HTTP_SPECIAL_RESPONSE) {
return rc;
}
/* handler exits — callback will finalize when body arrives */
return NGX_DONE;
}Increment Request Count Before Async Operations
nginx uses r->main->count as a reference counter. Before starting async operations (subrequests, timers, external I/O), increment the count to prevent the request from being destroyed while async work is pending. Forgetting this causes use-after-free when the timer or callback fires on a freed request.
Incorrect (timer callback on freed request):
static void
ngx_http_mymodule_timer_handler(ngx_event_t *ev)
{
ngx_http_request_t *r = ev->data;
/* BUG: r may already be freed — count was never incremented */
ngx_http_finalize_request(r, NGX_OK);
}
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_event_t *wev;
/* BUG: no r->main->count++ — request can be destroyed before timer */
wev = r->connection->write;
wev->handler = ngx_http_mymodule_timer_handler;
wev->data = r;
ngx_add_timer(wev, 5000);
return NGX_DONE;
}Correct (increment count before async, decrement on completion):
static void
ngx_http_mymodule_timer_handler(ngx_event_t *ev)
{
ngx_http_request_t *r = ev->data;
/* count was incremented — r is guaranteed alive */
ngx_http_finalize_request(r, NGX_OK);
/* finalize decrements count internally */
}
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_event_t *wev;
/* keep request alive while timer is pending */
r->main->count++;
wev = r->connection->write;
wev->handler = ngx_http_mymodule_timer_handler;
wev->data = r;
ngx_add_timer(wev, 5000);
return NGX_DONE;
}Discard Request Body When Not Reading It
If a handler does not need the request body, it must call ngx_http_discard_request_body. Without this call, the client blocks waiting to send its body and the connection hangs until timeout. This affects all methods that may carry a body (POST, PUT, PATCH).
Incorrect (ignoring request body causes client hang):
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_int_t rc;
ngx_buf_t *b;
ngx_chain_t out;
/* BUG: no discard — POST clients hang waiting to send body */
r->headers_out.status = NGX_HTTP_OK;
r->headers_out.content_length_n = sizeof("OK") - 1;
ngx_str_set(&r->headers_out.content_type, "text/plain");
rc = ngx_http_send_header(r);
if (rc == NGX_ERROR || rc > NGX_OK) {
return rc;
}
b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
if (b == NULL) {
return NGX_ERROR;
}
b->pos = (u_char *) "OK";
b->last = b->pos + sizeof("OK") - 1;
b->memory = 1;
b->last_buf = 1;
out.buf = b;
out.next = NULL;
return ngx_http_output_filter(r, &out);
}Correct (discard body before generating response):
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_int_t rc;
ngx_buf_t *b;
ngx_chain_t out;
/* discard body — unblocks client, drains connection */
rc = ngx_http_discard_request_body(r);
if (rc != NGX_OK) {
return rc;
}
r->headers_out.status = NGX_HTTP_OK;
r->headers_out.content_length_n = sizeof("OK") - 1;
ngx_str_set(&r->headers_out.content_type, "text/plain");
rc = ngx_http_send_header(r);
if (rc == NGX_ERROR || rc > NGX_OK) {
return rc;
}
b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
if (b == NULL) {
return NGX_ERROR;
}
b->pos = (u_char *) "OK";
b->last = b->pos + sizeof("OK") - 1;
b->memory = 1;
b->last_buf = 1;
out.buf = b;
out.next = NULL;
return ngx_http_output_filter(r, &out);
}Finalize Requests Exactly Once
ngx_http_finalize_request decrements r->main->count and may destroy the request and its pool when the count reaches zero. Calling finalize and then continuing to execute code that accesses r causes use-after-free. Even when the count is above zero, a second finalize double-decrements the counter, leading to premature request destruction. Always return immediately after calling finalize.
Incorrect (double finalize on error path):
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_int_t rc;
r->headers_out.status = NGX_HTTP_OK;
rc = ngx_http_send_header(r);
if (rc == NGX_ERROR) {
/* BUG: finalize then fall through to second finalize */
ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
}
/* BUG: r may already be freed if first finalize ran */
r->headers_out.content_length_n = 0;
ngx_http_finalize_request(r, rc);
return NGX_DONE;
}Correct (single finalize with immediate return):
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_int_t rc;
r->headers_out.status = NGX_HTTP_OK;
r->headers_out.content_length_n = 0;
rc = ngx_http_send_header(r);
if (rc == NGX_ERROR || rc > NGX_OK) {
ngx_http_finalize_request(r, rc);
return NGX_DONE;
}
/* all headers set BEFORE finalize — return immediately after */
ngx_http_finalize_request(r, ngx_http_output_filter(r, NULL));
return NGX_DONE;
}Return After Internal Redirect
ngx_http_internal_redirect starts a new request phase cycle, re-running the location matching and handler chain from the beginning. The current handler must return NGX_DONE immediately -- any subsequent processing generates a double response or corrupts the output.
Incorrect (continuing after internal redirect):
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_int_t rc;
if (needs_redirect(r)) {
rc = ngx_http_internal_redirect(r, &new_uri, &r->args);
/* BUG: falls through — generates second response */
}
/* this runs even after redirect, corrupting output */
r->headers_out.status = NGX_HTTP_OK;
r->headers_out.content_length_n = body.len;
ngx_http_send_header(r);
return ngx_http_output_filter(r, &out);
}Correct (return NGX_DONE immediately after redirect):
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_int_t rc;
if (needs_redirect(r)) {
rc = ngx_http_internal_redirect(r, &new_uri, &r->args);
if (rc != NGX_OK) {
return NGX_ERROR;
}
/* redirect started — exit immediately, new phase cycle handles it */
return NGX_DONE;
}
r->headers_out.status = NGX_HTTP_OK;
r->headers_out.content_length_n = body.len;
rc = ngx_http_send_header(r);
if (rc == NGX_ERROR || rc > NGX_OK) {
return rc;
}
return ngx_http_output_filter(r, &out);
}Never Access Request After Finalization
After ngx_http_finalize_request, the request pool may be destroyed and r, r->pool, and r->connection become dangling pointers. Any access is undefined behavior that manifests as intermittent crashes under load when the memory is reused.
Incorrect (accessing request after finalize):
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_http_finalize_request(r, NGX_OK);
/* BUG: r->pool may already be destroyed — use-after-free */
ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
"finished processing %V", &r->uri);
return NGX_DONE;
}Correct (capture data before finalize):
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_log_t *log;
/* capture anything needed BEFORE finalize */
log = r->connection->log;
ngx_log_error(NGX_LOG_INFO, log, 0,
"finishing request for %V", &r->uri);
/* finalize is the LAST operation — never touch r after this */
ngx_http_finalize_request(r, NGX_OK);
return NGX_DONE;
}Use Post-Subrequest Handlers for Completion
ngx_http_subrequest is asynchronous -- the subrequest may complete immediately or after network I/O. Always use ngx_http_post_subrequest_t to handle completion. Reading the subrequest response inline produces empty or stale data because the upstream has not responded yet.
Incorrect (reading subrequest response immediately):
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_http_request_t *sr;
ngx_int_t rc;
rc = ngx_http_subrequest(r, &uri, NULL, &sr, NULL, 0);
if (rc != NGX_OK) {
return NGX_ERROR;
}
/* BUG: subrequest has not completed yet — sr->headers_out is empty */
if (sr->headers_out.status == NGX_HTTP_OK) {
r->headers_out.status = NGX_HTTP_OK;
}
return NGX_OK;
}Correct (process results in post-subrequest handler):
static ngx_int_t
ngx_http_mymodule_subrequest_done(ngx_http_request_t *r,
void *data, ngx_int_t rc)
{
ngx_http_request_t *pr = r->parent;
/* subrequest finished — safe to read its response */
pr->headers_out.status = r->headers_out.status;
return NGX_OK;
}
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_http_request_t *sr;
ngx_http_post_subrequest_t *ps;
ps = ngx_palloc(r->pool, sizeof(ngx_http_post_subrequest_t));
if (ps == NULL) {
return NGX_ERROR;
}
ps->handler = ngx_http_mymodule_subrequest_done;
ps->data = NULL;
return ngx_http_subrequest(r, &uri, NULL, &sr, ps,
NGX_HTTP_SUBREQUEST_IN_MEMORY);
}Enable Keepalive for Upstream Connections
Each new upstream connection incurs TCP handshake overhead (1 RTT) plus optional TLS negotiation (1-2 additional RTTs). For a C module implementing an upstream protocol, the module must use HTTP/1.1 and avoid sending Connection: close to allow the built-in ngx_http_upstream_keepalive_module to pool connections. Sending Connection: close or using HTTP/1.0 forces a new TCP connection per request.
Incorrect (HTTP/1.0 with Connection: close prevents reuse):
static ngx_int_t
ngx_http_myproxy_create_request(ngx_http_request_t *r)
{
ngx_http_upstream_t *u = r->upstream;
ngx_buf_t *b;
size_t len;
/* HTTP/1.0 + Connection: close — every request opens new TCP connection */
len = sizeof("GET ") - 1 + r->uri.len
+ sizeof(" HTTP/1.0\r\nConnection: close\r\n\r\n") - 1;
b = ngx_create_temp_buf(r->pool, len);
if (b == NULL) {
return NGX_ERROR;
}
b->last = ngx_cpymem(b->last, "GET ", 4);
b->last = ngx_cpymem(b->last, r->uri.data, r->uri.len);
b->last = ngx_cpymem(b->last, " HTTP/1.0\r\n", 11);
b->last = ngx_cpymem(b->last, "Connection: close\r\n", 19);
b->last = ngx_cpymem(b->last, "\r\n", 2);
u->request_bufs = ngx_alloc_chain_link(r->pool);
if (u->request_bufs == NULL) {
return NGX_ERROR;
}
u->request_bufs->buf = b;
u->request_bufs->next = NULL;
return NGX_OK;
}Correct (HTTP/1.1 enables connection pooling by the keepalive module):
static ngx_int_t
ngx_http_myproxy_create_request(ngx_http_request_t *r)
{
ngx_http_upstream_t *u = r->upstream;
ngx_buf_t *b;
size_t len;
if (r->headers_in.host == NULL) {
return NGX_ERROR;
}
/* HTTP/1.1 defaults to keep-alive — no Connection: close header */
len = sizeof("GET ") - 1 + r->uri.len
+ sizeof(" HTTP/1.1\r\nHost: ") - 1
+ r->headers_in.host->value.len
+ sizeof("\r\n\r\n") - 1;
b = ngx_create_temp_buf(r->pool, len);
if (b == NULL) {
return NGX_ERROR;
}
b->last = ngx_cpymem(b->last, "GET ", 4);
b->last = ngx_cpymem(b->last, r->uri.data, r->uri.len);
b->last = ngx_cpymem(b->last, " HTTP/1.1\r\nHost: ", 18);
b->last = ngx_cpymem(b->last, r->headers_in.host->value.data,
r->headers_in.host->value.len);
b->last = ngx_cpymem(b->last, "\r\n\r\n", 4);
u->request_bufs = ngx_alloc_chain_link(r->pool);
if (u->request_bufs == NULL) {
return NGX_ERROR;
}
u->request_bufs->buf = b;
u->request_bufs->next = NULL;
return NGX_OK;
}Note: The keepalive directive in nginx.conf is handled by ngx_http_upstream_keepalive_module — you do not implement it in your C module. Your module's responsibility is to use HTTP/1.1 (which defaults to persistent connections) and avoid sending Connection: close. The keepalive module will then automatically pool idle connections for reuse across requests.
Build Complete Request Buffer in create_request
The create_request callback must produce a complete, valid protocol message in u->request_bufs. An incomplete buffer -- missing terminators, partial headers, or wrong content length -- causes the upstream to reject the request, reset the connection, or hang waiting for more data.
Incorrect (partial request buffer missing HTTP terminator):
static ngx_int_t
ngx_http_myproxy_create_request(ngx_http_request_t *r)
{
ngx_http_upstream_t *u = r->upstream;
ngx_buf_t *b;
size_t len;
len = sizeof("GET ") - 1 + r->uri.len + sizeof(" HTTP/1.0\r\n") - 1;
b = ngx_create_temp_buf(r->pool, len);
if (b == NULL) {
return NGX_ERROR;
}
/* BUG: missing final \r\n — upstream sees incomplete HTTP request */
b->last = ngx_cpymem(b->last, "GET ", 4);
b->last = ngx_cpymem(b->last, r->uri.data, r->uri.len);
b->last = ngx_cpymem(b->last, " HTTP/1.0\r\n", 11);
u->request_bufs = ngx_alloc_chain_link(r->pool);
if (u->request_bufs == NULL) {
return NGX_ERROR;
}
u->request_bufs->buf = b;
u->request_bufs->next = NULL;
return NGX_OK;
}Correct (complete request with Host header and double CRLF terminator):
static ngx_int_t
ngx_http_myproxy_create_request(ngx_http_request_t *r)
{
ngx_http_upstream_t *u = r->upstream;
ngx_buf_t *b;
size_t len;
if (r->headers_in.host == NULL) {
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
"no Host header in request");
return NGX_ERROR;
}
len = sizeof("GET ") - 1 + r->uri.len + sizeof(" HTTP/1.0\r\n") - 1
+ sizeof("Host: ") - 1 + r->headers_in.host->value.len
+ sizeof("\r\n") - 1
+ sizeof("\r\n") - 1; /* empty line terminates headers */
b = ngx_create_temp_buf(r->pool, len);
if (b == NULL) {
return NGX_ERROR;
}
b->last = ngx_cpymem(b->last, "GET ", 4);
b->last = ngx_cpymem(b->last, r->uri.data, r->uri.len);
b->last = ngx_cpymem(b->last, " HTTP/1.0\r\n", 11);
b->last = ngx_cpymem(b->last, "Host: ", 6);
b->last = ngx_cpymem(b->last, r->headers_in.host->value.data,
r->headers_in.host->value.len);
b->last = ngx_cpymem(b->last, "\r\n\r\n", 4);
u->request_bufs = ngx_alloc_chain_link(r->pool);
if (u->request_bufs == NULL) {
return NGX_ERROR;
}
u->request_bufs->buf = b;
u->request_bufs->next = NULL;
return NGX_OK;
}Clean Up Resources in finalize_request Callback
The finalize_request callback is called when nginx finishes with the upstream connection, including error cases (timeouts, resets, protocol errors). Any module-specific resources allocated during the upstream exchange -- open file descriptors, heap memory, external connections -- must be released here. Without cleanup, every failed upstream request leaks resources until the worker restarts.
Incorrect (allocates resources without cleanup in finalize):
static ngx_int_t
ngx_http_myproxy_create_request(ngx_http_request_t *r)
{
ngx_http_myproxy_ctx_t *ctx;
ctx = ngx_http_get_module_ctx(r, ngx_http_myproxy_module);
/* heap allocation for large temp buffer */
ctx->work_buf = ngx_alloc(NGX_HTTP_MYPROXY_BUFSIZE, r->connection->log);
ctx->temp_fd = ngx_open_tempfile(ctx->path.data, 0, 0);
/* BUG: if upstream fails, work_buf and temp_fd are never cleaned up */
return NGX_OK;
}
static void
ngx_http_myproxy_finalize_request(ngx_http_request_t *r, ngx_int_t rc)
{
/* empty — resources leak on every upstream error */
}Correct (finalize_request releases all allocated resources):
static void
ngx_http_myproxy_finalize_request(ngx_http_request_t *r, ngx_int_t rc)
{
ngx_http_myproxy_ctx_t *ctx;
ctx = ngx_http_get_module_ctx(r, ngx_http_myproxy_module);
if (ctx == NULL) {
return;
}
if (ctx->work_buf != NULL) {
ngx_free(ctx->work_buf);
ctx->work_buf = NULL;
}
if (ctx->temp_fd != NGX_INVALID_FILE) {
ngx_close_file(ctx->temp_fd);
ctx->temp_fd = NGX_INVALID_FILE;
}
ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
"myproxy finalize: rc=%d", rc);
}Track Failures in Peer free Callback
The peer free callback receives a state bitmask indicating how the connection ended. Track failures to avoid hammering permanently down backends. Decrement pc->tries to control whether nginx retries the request on another peer. Ignoring the state parameter causes infinite retries against dead backends, wasting connections and delaying client responses.
Incorrect (free callback ignores state parameter):
static void
ngx_http_mybalancer_free_peer(ngx_peer_connection_t *pc, void *data,
ngx_uint_t state)
{
ngx_http_mybalancer_peer_data_t *bp = data;
/* BUG: ignores failure state — dead backend stays in rotation */
bp->current = (bp->current + 1) % bp->num_peers;
/* BUG: pc->tries is never decremented — nginx retries forever */
}Correct (tracks failures and adjusts retry count):
static void
ngx_http_mybalancer_free_peer(ngx_peer_connection_t *pc, void *data,
ngx_uint_t state)
{
ngx_http_mybalancer_peer_data_t *bp = data;
ngx_http_mybalancer_peer_t *peer;
peer = &bp->peers[bp->current];
if (state & NGX_PEER_FAILED) {
peer->fails++;
peer->last_failed = ngx_time();
/* mark peer unavailable after threshold */
if (peer->fails >= peer->max_fails) {
peer->down = 1;
}
}
if (pc->tries > 0) {
pc->tries--;
}
}Parse Upstream Response Incrementally in process_header
The process_header callback may be called with partial data -- the upstream's response might arrive in multiple TCP segments. Return NGX_AGAIN if the headers are not yet complete. nginx will call process_header again when more data arrives in the buffer. Assuming the entire header is present on the first call causes truncated parsing and garbage values.
Incorrect (assumes complete headers in buffer):
static ngx_int_t
ngx_http_myproxy_process_header(ngx_http_request_t *r)
{
ngx_http_upstream_t *u = r->upstream;
u_char *p;
/* BUG: header may be split across reads — \r\n\r\n not yet in buffer */
p = u->buffer.pos;
/* blindly parses status line without checking for terminator */
u->headers_in.status_n = ngx_atoi(p + 9, 3);
u->headers_in.status_line.data = p + 9;
u->headers_in.status_line.len = 3;
u->buffer.pos = ngx_strlchr(p, u->buffer.last, '\n') + 1;
return NGX_OK;
}Correct (checks for complete header before parsing):
static ngx_int_t
ngx_http_myproxy_process_header(ngx_http_request_t *r)
{
ngx_http_upstream_t *u = r->upstream;
u_char *p, *end;
end = ngx_strnstr(u->buffer.pos, "\r\n\r\n",
u->buffer.last - u->buffer.pos);
if (end == NULL) {
/* headers incomplete — wait for more data from upstream */
return NGX_AGAIN;
}
/* safe to parse — full header block is in the buffer */
p = u->buffer.pos;
if (u->buffer.last - p < 12) {
return NGX_HTTP_UPSTREAM_INVALID_HEADER;
}
u->headers_in.status_n = ngx_atoi(p + 9, 3);
if (u->headers_in.status_n == NGX_ERROR) {
return NGX_HTTP_UPSTREAM_INVALID_HEADER;
}
u->headers_in.status_line.data = p + 9;
u->headers_in.status_line.len = 3;
u->buffer.pos = end + 4; /* skip past \r\n\r\n */
return NGX_OK;
}Related skills
FAQ
What does nginx-c-modules do?
nginx-c-modules: A skill for development. This provides functionality for development workflows.
When should I use nginx-c-modules?
When you need to use nginx-c-modules for development tasks, or when nginx-c-modules: a skill for development. this provides functionality for development workflows.
What are the main capabilities?
nginx-c-modules.