
Nginx C Module Design
- 253 installs
- 191 repo stars
- Updated July 24, 2026
- pproenca/dot-skills
nginx-c-module-design: A skill for development. This provides functionality for development workflows.
Key points
- nginx-c-module-design
Nginx C Module Design by the numbers
- 253 all-time installs (skills.sh)
- +10 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,518 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-module-designAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 253 |
|---|---|
| repo stars | ★ 191 |
| Last updated | July 24, 2026 |
| Repository | pproenca/dot-skills ↗ |
How do I use nginx-c-module-design for development tasks?
Use nginx-c-module-design for development tasks
Who is it for?
Best when you're working on backend & apis and need structured help with nginx-c-module-design.
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-module-design for development tasks, or when nginx-c-module-design: a skill for development. this provides functionality for development workflows.
What you get
Structured output aligned to nginx-c-module-design: nginx-c-module-design.
Files
nginx.org C Module Directive Design Best Practices
Comprehensive directive design guide for nginx C module authors, focused on creating clear, consistent, and admin-friendly configuration interfaces. Contains 46 rules across 8 categories, prioritized by impact to guide decisions about what to expose, how to name it, and how to evolve it safely.
When to Apply
Reference these guidelines when:
- Deciding which values to expose as directives vs hardcode
- Naming new directives and choosing argument types
- Selecting scope placement (http, server, location)
- Setting default values and validation behavior
- Designing nginx variables for runtime data
- Deprecating or renaming existing directives
Companion Skills
This skill focuses on design decisions (the "what" and "why"). For implementation mechanics, see:
- nginx-c-modules — C implementation: memory pools, request lifecycle, config parsing, handlers, filters
- nginx-c-perf — Performance: buffers, connections, locks, caching, timeouts
- nginx-c-debug — Debugging: crash diagnosis, GDB, tracing, sanitizers
Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Exposure Decisions | CRITICAL | expose- |
| 2 | Naming Conventions | CRITICAL | naming- |
| 3 | Directive Types | HIGH | type- |
| 4 | Scope Design | HIGH | scope- |
| 5 | Default Values | MEDIUM-HIGH | default- |
| 6 | Validation & Error Messages | MEDIUM | valid- |
| 7 | Variable Design | MEDIUM | var- |
| 8 | Evolution & Compatibility | LOW-MEDIUM | compat- |
Quick Reference
1. Exposure Decisions (CRITICAL)
- `expose-configurable-vs-hardcode` - Framework for Configurable vs Hardcoded Values
- `expose-escape-hatch` - Provide Escape Hatches for Hardcoded Defaults
- `expose-feature-gate` - Use Feature Gates for Optional Behavior
- `expose-too-many-directives` - Avoid Over-Configuration
- `expose-path-resource` - Always Expose External Resource Paths
- `expose-security-surface` - Audit Security Implications of Every Exposed Directive
- `expose-environment-dependent` - Expose Values That Vary by Deployment Environment
2. Naming Conventions (CRITICAL)
- `naming-module-prefix` - Use a Consistent Module Prefix for All Directives
- `naming-sub-prefix-groups` - Group Related Directives with Sub-Prefixes
- `naming-noun-over-verb` - Prefer Noun Phrases for Directive Names
- `naming-no-abbreviations` - Avoid Custom Abbreviations in Directive Names
- `naming-cross-module-consistency` - Mirror Nginx Core Suffix Patterns for Analogous Directives
- `naming-lowercase-underscore` - Use Lowercase with Underscores Only
3. Directive Types (HIGH)
- `type-flag-for-toggles` - Use NGX_CONF_FLAG for Binary Toggles
- `type-enum-over-string` - Use Enum Slot for Known Value Sets
- `type-time-size-units` - Use Time and Size Slot Functions for Time and Size Values
- `type-take-n-fixed-args` - Use TAKE1/TAKE2/TAKE12 for Fixed Argument Counts
- `type-one-more-lists` - Use 1MORE for Variable-Length Value Lists
- `type-avoid-block` - Avoid Block Directives for Features
- `type-custom-handler-complex` - Use Custom Handlers for Complex Directive Parsing
4. Scope Design (HIGH)
- `scope-default-three-levels` - Default to http + server + location Scope
- `scope-http-only-shared-resources` - Restrict Shared Resource Directives to http Level Only
- `scope-server-connection-level` - Use http + server Scope for Connection-Level Settings
- `scope-avoid-if-context` - Do Not Support the if Context Unless Fully Tested
- `scope-location-path-operations` - Restrict Path-Routing Directives to Location Context
5. Default Values (MEDIUM-HIGH)
- `default-zero-config-safe` - Ensure Zero-Config Produces Safe Behavior
- `default-performance-optin` - Make Performance Features Opt-In
- `default-safety-on` - Default Security Settings to Restrictive Values
- `default-generous-timeouts` - Default Timeouts to Generous Values
- `default-zero-unlimited` - Use Zero to Mean Unlimited or Disabled for Numeric Limits
- `default-platform-aware-buffers` - Use Platform-Aware Buffer Size Defaults
6. Validation & Error Messages (MEDIUM)
- `valid-parse-time-check` - Validate All Directive Values at Config Parse Time
- `valid-show-invalid-value` - Include the Invalid Value in Error Messages
- `valid-suggest-range` - Include Valid Range or Format in Error Messages
- `valid-conflict-detection` - Detect Conflicting Directives at Merge Time
- `valid-actionable-guidance` - Provide Actionable Guidance in Error Messages
7. Variable Design (MEDIUM)
- `var-runtime-data-only` - Expose Variables for Per-Request Runtime Data Only
- `var-naming-convention` - Name Variables with Module Prefix and Descriptive Suffix
- `var-dynamic-prefix` - Use Dynamic Prefix Variables for Key-Value Data
- `var-lazy-evaluation` - Leverage Lazy Evaluation for Expensive Variables
- `var-in-directive-values` - Support Variables in Directive Values Only When Per-Request Variation Is Needed
- `var-read-only-diagnostics` - Expose Read-Only Diagnostic Variables for Observability
8. Evolution & Compatibility (LOW-MEDIUM)
- `compat-deprecation-warning` - Log Warnings for Deprecated Directives Before Removal
- `compat-alias-old-directive` - Keep Old Directive Name as an Alias
- `compat-multi-version-window` - Maintain a Multi-Version Deprecation Window
- `compat-document-migration` - Document Migration Path in Both Old and New Directive Documentation
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}
{1-3 sentences explaining WHY this matters for nginx admins. Focus on the admin experience and configuration clarity.}
Incorrect ({what's wrong}):
/* ngx_command_t entry */
{Bad directive design — production-realistic, not strawman}# nginx.conf usage showing the admin-facing problem
{Bad config example showing confusion or error}Correct ({what's right}):
/* ngx_command_t entry */
{Good directive design — minimal diff from incorrect}# nginx.conf usage showing clear admin experience
{Good config example showing clarity}{Optional sections as needed:}
When NOT to use this pattern:
- {Exception 1}
- {Exception 2}
Benefits:
- {Benefit 1 — from the admin's perspective}
- {Benefit 2 — from the module author's perspective}
Reference: [{Reference Title}]({Reference URL})
{
"version": "1.0.5",
"organization": "nginx.org",
"technology": "nginx C Module Directive Design",
"date": "February 2026",
"abstract": "Comprehensive directive design guide for nginx C module authors, focused on creating admin-friendly configuration interfaces. Contains 46 rules across 8 categories, prioritized by impact from critical (exposure decisions, naming conventions) to incremental (evolution and compatibility). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.",
"references": [
"https://nginx.org/en/docs/dev/development_guide.html",
"https://www.evanmiller.org/nginx-modules-guide.html",
"https://github.com/nginx/nginx/blob/master/src/http/modules/ngx_http_proxy_module.c",
"https://github.com/nginx/nginx/blob/master/src/http/modules/ngx_http_ssl_module.c",
"https://github.com/nginx/nginx/blob/master/src/http/modules/ngx_http_fastcgi_module.c",
"https://github.com/openresty/lua-nginx-module",
"https://nginx.org/en/docs/http/ngx_http_proxy_module.html",
"https://nginx.org/en/docs/http/ngx_http_core_module.html"
]
}
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. Exposure Decisions (expose)
Impact: CRITICAL Description: Deciding what to make configurable vs hardcode is the most consequential design choice — wrong exposure creates security holes, admin confusion, or forces workarounds when needed settings are compiled in.
2. Naming Conventions (naming)
Impact: CRITICAL Description: Directive naming is the module's public API — inconsistent prefixes, wrong verb/noun patterns, or unclear grouping makes a working module look broken and untrustworthy to nginx admins.
3. Directive Types (type)
Impact: HIGH Description: Choosing the wrong directive type (flag vs enum, raw number vs time unit, string vs enum slot) creates confusing syntax that fights nginx admin muscle memory.
4. Scope Design (scope)
Impact: HIGH Description: Wrong scope placement breaks config inheritance, prevents per-location tuning, or makes directives silently ignored — the admin sees no error but the directive has no effect.
5. Default Values (default)
Impact: MEDIUM-HIGH Description: Bad defaults cause zero-config deployments to be insecure, unreliable, or silently degraded — every directive must work safely when the admin provides no explicit value.
6. Validation & Error Messages (valid)
Impact: MEDIUM Description: Poor validation defers errors from config-test time to request time, and unhelpful messages waste hours of admin debugging — parse-time validation with actionable messages is essential.
7. Variable Design (var)
Impact: MEDIUM Description: Exposing the wrong data as nginx variables creates misleading diagnostics, performance traps from expensive evaluation, or admin confusion about what belongs in directives vs variables.
8. Evolution & Compatibility (compat)
Impact: LOW-MEDIUM Description: Breaking directive changes without deprecation paths destroy admin trust, break automation tooling, and force emergency config rewrites during upgrades.
Keep Old Directive Name as an Alias
When renaming a directive, register both the old and new names pointing to the same handler and config field offset. The old name triggers a deprecation warning but produces identical behavior. This allows gradual migration of automation, Ansible playbooks, and config management systems without emergency config rewrites. Pattern from nginx: ssl on|off was deprecated in 1.15.0 in favor of listen ... ssl, and kept working for 5 years until removal in 1.25.1.
Incorrect (renaming a directive with no alias breaks all existing configs):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* v2.0: renamed from "mymod_max_conns" to "mymod_connection_limit"
* with no alias — every existing config breaks on upgrade */
{ ngx_string("mymod_connection_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, connection_limit),
NULL },
ngx_null_command
};Correct (both old and new names registered, old one logs a deprecation warning):
static char *
ngx_http_mymodule_max_conns_deprecated(ngx_conf_t *cf, ngx_command_t *cmd,
void *conf)
{
ngx_http_mymodule_loc_conf_t *mlcf = conf;
ngx_str_t *value;
ngx_conf_log_error(NGX_LOG_WARN, cf, 0,
"the \"mymod_max_conns\" directive is deprecated, "
"use \"mymod_connection_limit\" instead");
value = cf->args->elts;
mlcf->connection_limit = ngx_atoi(value[1].data, value[1].len);
if (mlcf->connection_limit == NGX_ERROR) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid value \"%V\" in \"%V\" directive",
&value[1], &cmd->name);
return NGX_CONF_ERROR;
}
return NGX_CONF_OK;
}
static ngx_command_t ngx_http_mymodule_commands[] = {
/* new canonical name */
{ ngx_string("mymod_connection_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, connection_limit),
NULL },
/* old name kept as alias — same field, warns on use */
{ ngx_string("mymod_max_conns"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_http_mymodule_max_conns_deprecated,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
ngx_null_command
};Log Warnings for Deprecated Directives Before Removal
When deprecating a directive, log a warning using ngx_conf_log_error(NGX_LOG_WARN, ...) but continue to honor the old directive's behavior. The admin sees the warning during nginx -t and reload, giving them time to update their config. Never remove a directive without a warning period — nginx deprecated ssl on|off in 1.15.0 (2018) and removed it in 1.25.1 (2023), a 5-year window.
Incorrect (silently removing a directive causes config parse failures on upgrade):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* The old "mymod_backend" directive was removed entirely in v3.0 —
* any existing config using it now fails with "unknown directive" */
{ ngx_string("mymod_upstream"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_http_mymodule_set_upstream,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
ngx_null_command
};Correct (logging a deprecation warning while still honoring the old directive):
static char *
ngx_http_mymodule_set_backend_deprecated(ngx_conf_t *cf, ngx_command_t *cmd,
void *conf)
{
ngx_conf_log_error(NGX_LOG_WARN, cf, 0,
"the \"mymod_backend\" directive is deprecated, "
"use \"mymod_upstream\" instead");
/* delegate to the new handler — identical behavior */
return ngx_http_mymodule_set_upstream(cf, cmd, conf);
}
static ngx_command_t ngx_http_mymodule_commands[] = {
/* new canonical name */
{ ngx_string("mymod_upstream"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_http_mymodule_set_upstream,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
/* deprecated alias — warns but still works */
{ ngx_string("mymod_backend"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_http_mymodule_set_backend_deprecated,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
ngx_null_command
};Document Migration Path in Both Old and New Directive Documentation
When a directive is deprecated in favor of a new one, document the migration in both places: the old directive's docs should say "use X instead" with an example, and the new directive's docs should say "replaces Y" with a before/after example. This ensures admins find the migration path regardless of which documentation they consult.
Incorrect (only mentioning the deprecation in the changelog, not in directive documentation):
# CHANGELOG.md says:
# v3.0: Replaced mymod_rate with mymod_rate_limit
#
# But the directive documentation has no cross-reference:
# --- mymod_rate_limit docs ---
# Syntax: mymod_rate_limit rate
# Default: 100r/s
# Context: http, server, location
#
# Sets the request rate limit.
#
# (no mention that this replaces mymod_rate,
# admin using mymod_rate has no idea this exists
# unless they read the full changelog)Correct (both old and new directive docs cross-reference each other with migration examples):
# --- mymod_rate docs (deprecated) ---
# Syntax: mymod_rate number
# Default: 100
# Context: http, server, location
#
# This directive is deprecated since v3.0 and will be removed in v6.0.
# Use mymod_rate_limit instead.
#
# Migration:
# # Before (v2.x):
# mymod_rate 500;
#
# # After (v3.0+):
# mymod_rate_limit 500r/s;
# --- mymod_rate_limit docs ---
# Syntax: mymod_rate_limit rate
# Default: 100r/s
# Context: http, server, location
# Appeared in: v3.0
#
# Sets the request rate limit. Replaces the deprecated mymod_rate
# directive, adding support for rate unit suffixes (r/s, r/m).
#
# Migration from mymod_rate:
# # Before (v2.x) — raw number, always per-second:
# mymod_rate 500;
#
# # After (v3.0+) — explicit unit:
# mymod_rate_limit 500r/s;Maintain a Multi-Version Deprecation Window
Deprecated directives must continue working for at least 3 major versions (or 2+ years for time-based releases) before removal. Many nginx deployments run behind the latest version, and config management systems need time to update. Pattern: nginx kept listen ... http2 working alongside the new http2 on/off directive for multiple versions before deprecating the old syntax.
Incorrect (removing a directive in the very next release after deprecation):
/*
* v2.0 changelog: "mymod_cache_size is deprecated, use mymod_cache_max"
* v2.1 changelog: "mymod_cache_size has been removed"
*
* Admins upgrading from v1.x to v2.1 see:
* nginx: [emerg] unknown directive "mymod_cache_size"
* nginx: configuration file test failed
*/
static ngx_command_t ngx_http_mymodule_commands[] = {
/* mymod_cache_size was removed in v2.1 — only one minor version
* after deprecation, no time for admins to migrate */
{ ngx_string("mymod_cache_max"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_size_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, cache_max),
NULL },
ngx_null_command
};Correct (deprecated directive maintained for 3+ versions with visible warning, then removed with a clear changelog entry):
/*
* v2.0: mymod_cache_size deprecated — logs NGX_LOG_WARN, behavior unchanged
* v3.0: mymod_cache_size still works — warning message updated to say
* "will be removed in v5.0"
* v4.0: mymod_cache_size still works — warning says "removed in v5.0"
* v5.0: mymod_cache_size removed — changelog entry with migration example
*/
static char *
ngx_http_mymodule_cache_size_deprecated(ngx_conf_t *cf, ngx_command_t *cmd,
void *conf)
{
ngx_http_mymodule_loc_conf_t *mlcf = conf;
ngx_str_t *value;
ngx_conf_log_error(NGX_LOG_WARN, cf, 0,
"the \"mymod_cache_size\" directive is deprecated "
"and will be removed in v5.0, "
"use \"mymod_cache_max\" instead");
value = cf->args->elts;
mlcf->cache_max = ngx_parse_size(&value[1]);
if (mlcf->cache_max == (size_t) NGX_ERROR) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid size \"%V\" in \"%V\" directive",
&value[1], &cmd->name);
return NGX_CONF_ERROR;
}
return NGX_CONF_OK;
}
static ngx_command_t ngx_http_mymodule_commands[] = {
{ ngx_string("mymod_cache_max"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_size_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, cache_max),
NULL },
/* kept for 3 major versions with removal date in warning */
{ ngx_string("mymod_cache_size"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_http_mymodule_cache_size_deprecated,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
ngx_null_command
};Default Timeouts to Generous Values
Timeout defaults should be generous enough to avoid false failures in typical deployments. Pattern from nginx: most timeouts default to 60s (proxy_connect_timeout, proxy_send_timeout, proxy_read_timeout, client_body_timeout). This is slow for optimized setups but prevents timeout errors for admins who haven't tuned their config yet. Admins tighten timeouts; they should not have to relax them from overly aggressive defaults.
Incorrect (5-second connect timeout causes failures for geographically distant upstreams):
typedef struct {
ngx_msec_t connect_timeout;
ngx_msec_t read_timeout;
ngx_msec_t send_timeout;
} ngx_http_mymodule_loc_conf_t;
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;
/* BUG: 5s connect timeout — cross-region upstreams routinely take
* 2-4s on cold connections, causing spurious 504s */
ngx_conf_merge_msec_value(conf->connect_timeout,
prev->connect_timeout, 5000);
/* BUG: 3s read timeout — large API responses from slow backends
* trigger timeouts under normal load */
ngx_conf_merge_msec_value(conf->read_timeout,
prev->read_timeout, 3000);
/* BUG: 2s send timeout — clients on mobile connections
* cannot receive data fast enough */
ngx_conf_merge_msec_value(conf->send_timeout,
prev->send_timeout, 2000);
return NGX_CONF_OK;
}Correct (60-second defaults match nginx core, letting admins tighten as needed):
typedef struct {
ngx_msec_t connect_timeout;
ngx_msec_t read_timeout;
ngx_msec_t send_timeout;
} ngx_http_mymodule_loc_conf_t;
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;
}
conf->connect_timeout = NGX_CONF_UNSET_MSEC;
conf->read_timeout = NGX_CONF_UNSET_MSEC;
conf->send_timeout = NGX_CONF_UNSET_MSEC;
return conf;
}
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;
/* 60s — matches proxy_connect_timeout, works for all deployments */
ngx_conf_merge_msec_value(conf->connect_timeout,
prev->connect_timeout, 60000);
/* 60s — matches proxy_read_timeout, handles slow backends */
ngx_conf_merge_msec_value(conf->read_timeout,
prev->read_timeout, 60000);
/* 60s — matches proxy_send_timeout, tolerates slow clients */
ngx_conf_merge_msec_value(conf->send_timeout,
prev->send_timeout, 60000);
return NGX_CONF_OK;
}Make Performance Features Opt-In
Features that consume additional resources (caching, shared memory, thread pools) or change behavior (buffering mode, compression) should be off by default and require explicit opt-in. Pattern from nginx: proxy_cache off, gzip off, ssl_stapling off. This ensures the module's resource footprint is zero until the admin explicitly enables features.
Incorrect (cache enabled by default, allocating shared memory on module load):
typedef struct {
ngx_shm_zone_t *cache_zone;
ngx_flag_t cache_enabled;
size_t cache_size;
} ngx_http_mymodule_main_conf_t;
static void *
ngx_http_mymodule_create_main_conf(ngx_conf_t *cf)
{
ngx_http_mymodule_main_conf_t *conf;
conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_mymodule_main_conf_t));
if (conf == NULL) {
return NULL;
}
conf->cache_enabled = NGX_CONF_UNSET;
conf->cache_size = NGX_CONF_UNSET_SIZE;
return conf;
}
static char *
ngx_http_mymodule_init_main_conf(ngx_conf_t *cf, void *conf)
{
ngx_http_mymodule_main_conf_t *mmcf = conf;
ngx_str_t name = ngx_string("mymod_cache");
/* BUG: cache defaults to ON — allocates 32MB shared memory even when
* nobody configured caching, wasting resources on every deployment */
ngx_conf_init_value(mmcf->cache_enabled, 1);
ngx_conf_init_size_value(mmcf->cache_size, 32 * 1024 * 1024);
mmcf->cache_zone = ngx_shared_memory_add(cf, &name,
mmcf->cache_size,
&ngx_http_mymodule_module);
if (mmcf->cache_zone == NULL) {
return NGX_CONF_ERROR;
}
return NGX_CONF_OK;
}Correct (cache disabled by default, no resources allocated until admin sets `mymod_cache_zone`):
typedef struct {
ngx_shm_zone_t *cache_zone;
size_t cache_size;
} ngx_http_mymodule_main_conf_t;
static void *
ngx_http_mymodule_create_main_conf(ngx_conf_t *cf)
{
ngx_http_mymodule_main_conf_t *conf;
conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_mymodule_main_conf_t));
if (conf == NULL) {
return NULL;
}
/* cache_zone is NULL from pcalloc — no resources until configured */
return conf;
}
/* cache zone is only created when admin explicitly configures it:
* mymod_cache_zone zone=mymod:16m;
*/
static char *
ngx_http_mymodule_cache_zone(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_mymodule_main_conf_t *mmcf = conf;
ngx_str_t name, s;
ngx_uint_t i;
for (i = 1; i < cf->args->nelts; i++) {
/* parse zone=name:size arguments */
}
mmcf->cache_zone = ngx_shared_memory_add(cf, &name, mmcf->cache_size,
&ngx_http_mymodule_module);
if (mmcf->cache_zone == NULL) {
return NGX_CONF_ERROR;
}
return NGX_CONF_OK;
}
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_http_mymodule_main_conf_t *mmcf;
mmcf = ngx_http_get_module_main_conf(r, ngx_http_mymodule_module);
/* no cache zone configured — skip caching entirely, zero overhead */
if (mmcf->cache_zone == NULL) {
return ngx_http_mymodule_pass_through(r);
}
return ngx_http_mymodule_cached_response(r, mmcf->cache_zone);
}Use Platform-Aware Buffer Size Defaults
Buffer size defaults should adapt to the platform's memory page size rather than being a fixed constant. Pattern from nginx: many buffer defaults are documented as 4k|8k (page-size dependent). Use ngx_pagesize at config time to align buffer allocations to the platform, avoiding partial-page I/O penalties.
Incorrect (hardcoded 4096 buffer size on all platforms):
typedef struct {
size_t buffer_size;
size_t read_buffer_size;
} ngx_http_mymodule_loc_conf_t;
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;
/* BUG: hardcoded 4096 — on platforms with 8k or 16k pages
* (ARM64 Linux, many BSDs), this causes partial-page reads
* and wastes a full page per allocation anyway */
ngx_conf_merge_size_value(conf->buffer_size,
prev->buffer_size, 4096);
/* BUG: 8192 is correct on some platforms, wrong on others */
ngx_conf_merge_size_value(conf->read_buffer_size,
prev->read_buffer_size, 8192);
return NGX_CONF_OK;
}Correct (using ngx_pagesize to set platform-aligned default buffer sizes):
typedef struct {
size_t buffer_size;
size_t read_buffer_size;
} ngx_http_mymodule_loc_conf_t;
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;
}
conf->buffer_size = NGX_CONF_UNSET_SIZE;
conf->read_buffer_size = NGX_CONF_UNSET_SIZE;
return conf;
}
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;
/* one page — 4k on x86, 8k on SPARC, 16k on ARM64 with 16k pages;
* matches nginx convention for single-buffer defaults */
ngx_conf_merge_size_value(conf->buffer_size,
prev->buffer_size, ngx_pagesize);
/* two pages — suitable for read-ahead buffers that benefit from
* larger I/O, still page-aligned on every platform */
ngx_conf_merge_size_value(conf->read_buffer_size,
prev->read_buffer_size,
2 * ngx_pagesize);
return NGX_CONF_OK;
}Default Security Settings to Restrictive Values
Directives that control access, validation, or security policy must default to the most restrictive safe setting. Pattern from nginx: ssl_protocols TLSv1.2 TLSv1.3 (rejects old TLS), ssl_ciphers HIGH:!aNULL:!MD5 (rejects weak ciphers), underscores_in_headers off (silently drops suspicious headers). Fail-open for functionality, fail-closed for security.
Incorrect (SSL verification disabled by default, accepting all cipher suites):
typedef struct {
ngx_flag_t verify_upstream;
ngx_str_t ciphers;
ngx_uint_t protocols;
} ngx_http_mymodule_loc_conf_t;
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;
/* BUG: verification OFF by default — upstream responses are
* trusted without any certificate validation */
ngx_conf_merge_value(conf->verify_upstream, prev->verify_upstream, 0);
/* BUG: accepts all ciphers including NULL and export-grade —
* connections are trivially interceptable */
ngx_conf_merge_str_value(conf->ciphers, prev->ciphers, "ALL");
/* BUG: allows SSLv3 and TLSv1.0 — known vulnerable protocols */
ngx_conf_merge_uint_value(conf->protocols, prev->protocols,
NGX_SSL_SSLv3|NGX_SSL_TLSv1
|NGX_SSL_TLSv1_1|NGX_SSL_TLSv1_2
|NGX_SSL_TLSv1_3);
return NGX_CONF_OK;
}Correct (restrictive defaults that require explicit relaxation):
typedef struct {
ngx_flag_t verify_upstream;
ngx_str_t ciphers;
ngx_uint_t protocols;
ngx_uint_t verify_depth;
} ngx_http_mymodule_loc_conf_t;
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;
}
conf->verify_upstream = NGX_CONF_UNSET;
conf->protocols = 0;
conf->verify_depth = NGX_CONF_UNSET_UINT;
return conf;
}
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;
/* verification ON by default — admin must explicitly disable */
ngx_conf_merge_value(conf->verify_upstream, prev->verify_upstream, 1);
/* only strong ciphers, reject NULL and weak suites */
ngx_conf_merge_str_value(conf->ciphers, prev->ciphers,
"HIGH:!aNULL:!MD5:!EXPORT");
/* only modern TLS — admin must explicitly add older protocols */
ngx_conf_merge_uint_value(conf->protocols, prev->protocols,
NGX_SSL_TLSv1_2|NGX_SSL_TLSv1_3);
/* reasonable depth prevents excessively long certificate chains */
ngx_conf_merge_uint_value(conf->verify_depth, prev->verify_depth, 1);
return NGX_CONF_OK;
}Ensure Zero-Config Produces Safe Behavior
A module loaded with no explicit directives must work safely. It may not be optimal, but it must not crash, corrupt data, or create security holes. Following the nginx pattern: proxy_buffering on (safe default), sendfile off (works everywhere, slower). The admin who just adds load_module should see correct behavior even if they haven't configured anything.
Incorrect (NULL pointer dereference when no directives are configured):
typedef struct {
ngx_str_t upstream_host;
ngx_uint_t max_retries;
} ngx_http_mymodule_loc_conf_t;
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: upstream_host is {NULL, 0} from pcalloc, max_retries is 0 —
* handler will dereference upstream_host.data and segfault */
return conf;
}
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_http_mymodule_loc_conf_t *mlcf;
mlcf = ngx_http_get_module_loc_conf(r, ngx_http_mymodule_module);
/* CRASH: upstream_host.data is NULL when no directive was set */
ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0,
"connecting to %V", &mlcf->upstream_host);
return ngx_http_mymodule_connect(r, &mlcf->upstream_host);
}Correct (safe compiled-in defaults work without any explicit directives):
typedef struct {
ngx_str_t upstream_host;
ngx_uint_t max_retries;
ngx_flag_t enabled;
} ngx_http_mymodule_loc_conf_t;
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;
}
conf->max_retries = NGX_CONF_UNSET_UINT;
conf->enabled = NGX_CONF_UNSET;
return conf;
}
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_str_value(conf->upstream_host, prev->upstream_host, "");
ngx_conf_merge_uint_value(conf->max_retries, prev->max_retries, 1);
ngx_conf_merge_value(conf->enabled, prev->enabled, 0);
return NGX_CONF_OK;
}
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_http_mymodule_loc_conf_t *mlcf;
mlcf = ngx_http_get_module_loc_conf(r, ngx_http_mymodule_module);
/* module is inactive until explicitly enabled — safe no-op */
if (!mlcf->enabled || mlcf->upstream_host.len == 0) {
return NGX_DECLINED;
}
return ngx_http_mymodule_connect(r, &mlcf->upstream_host);
}Use Zero to Mean Unlimited or Disabled for Numeric Limits
When a directive controls a numeric limit (retries, rate, connections), use 0 to mean "unlimited" or "disabled." This is the universal nginx convention: proxy_next_upstream_tries 0 (unlimited), limit_rate 0 (no limit), proxy_next_upstream_timeout 0 (no timeout). Do not use -1, MAX_INT, or other sentinel values.
Incorrect (sentinel value -1 to mean unlimited, breaking nginx convention):
typedef struct {
ngx_int_t max_retries;
ngx_int_t rate_limit;
} ngx_http_mymodule_loc_conf_t;
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: using -1 for unlimited — no nginx directive does this,
* admins will guess 0 and get "disabled" instead of "unlimited" */
conf->max_retries = -1;
conf->rate_limit = -1;
return conf;
}
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_http_mymodule_loc_conf_t *mlcf;
mlcf = ngx_http_get_module_loc_conf(r, ngx_http_mymodule_module);
/* confusing: -1 means unlimited, 0 means disabled, positive means limit —
* three-way semantics that no nginx admin expects */
if (mlcf->max_retries == -1) {
/* unlimited retries */
} else if (mlcf->max_retries == 0) {
/* no retries */
} else {
/* limited to max_retries */
}
return NGX_OK;
}Correct (0 means unlimited, matching nginx convention):
typedef struct {
ngx_uint_t max_retries;
ngx_uint_t rate_limit;
} ngx_http_mymodule_loc_conf_t;
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;
}
conf->max_retries = NGX_CONF_UNSET_UINT;
conf->rate_limit = NGX_CONF_UNSET_UINT;
return conf;
}
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;
/* 0 = unlimited retries, matches proxy_next_upstream_tries convention */
ngx_conf_merge_uint_value(conf->max_retries, prev->max_retries, 0);
/* 0 = no rate limit, matches limit_rate convention */
ngx_conf_merge_uint_value(conf->rate_limit, prev->rate_limit, 0);
return NGX_CONF_OK;
}
static ngx_int_t
ngx_http_mymodule_upstream_next(ngx_http_request_t *r, ngx_uint_t attempt)
{
ngx_http_mymodule_loc_conf_t *mlcf;
mlcf = ngx_http_get_module_loc_conf(r, ngx_http_mymodule_module);
/* clean two-way semantics: 0 means unlimited, >0 means limited */
if (mlcf->max_retries == 0 || attempt < mlcf->max_retries) {
return ngx_http_mymodule_retry(r);
}
return NGX_HTTP_BAD_GATEWAY;
}Framework for Configurable vs Hardcoded Values
Expose values that vary by deployment (timeouts, buffer sizes, paths, feature gates). Hardcode protocol constants, internal limits, and values with only one correct answer. The test: "Would a reasonable admin need a different value in staging vs production?"
Incorrect (hardcoded timeout forces recompilation to change it):
/* compile-time constant — admin must rebuild the module for every change */
#define MYMOD_TIMEOUT 60000
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_http_mymodule_loc_conf_t *mlcf;
mlcf = ngx_http_get_module_loc_conf(r, ngx_http_mymodule_module);
/* timeout is baked in — staging wants 5s, production wants 60s,
* neither can change it without recompiling */
ngx_add_timer(r->connection->write, MYMOD_TIMEOUT);
return NGX_OK;
}Correct (directive with UNSET init lets admins tune per-environment):
static ngx_command_t ngx_http_mymodule_commands[] = {
{ ngx_string("mymod_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
};
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 allows inheritance and merge with a sensible default */
conf->timeout = NGX_CONF_UNSET_MSEC;
return conf;
}
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);
return NGX_CONF_OK;
}Expose Values That Vary by Deployment Environment
If a value changes between development, staging, and production (connection limits, timeouts, TLS settings, log levels, backend addresses), it must be a directive. The goal is one compiled module binary that works across all environments via config alone.
Incorrect (compile-time ifdef creates different binaries per environment):
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: must recompile for each environment — cannot use the
* same binary in dev, staging, and production */
#ifdef DEBUG
conf->connect_timeout = 30000;
conf->verbose_errors = 1;
conf->max_connections = 10;
#else
conf->connect_timeout = 5000;
conf->verbose_errors = 0;
conf->max_connections = 1024;
#endif
return conf;
}Correct (all environment-varying values are directives with safe defaults):
static ngx_command_t ngx_http_mymodule_commands[] = {
{ ngx_string("mymod_connect_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, connect_timeout),
NULL },
{ ngx_string("mymod_verbose_errors"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, verbose_errors),
NULL },
{ ngx_string("mymod_max_connections"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
ngx_conf_set_num_slot,
NGX_HTTP_SRV_CONF_OFFSET,
offsetof(ngx_http_mymodule_srv_conf_t, max_connections),
NULL },
ngx_null_command
};
/* nginx.conf — same binary, different config per env:
*
* # dev.conf
* mymod_connect_timeout 30s;
* mymod_verbose_errors on;
* mymod_max_connections 10;
*
* # prod.conf
* mymod_connect_timeout 5s;
* mymod_verbose_errors off;
* mymod_max_connections 1024;
*/Provide Escape Hatches for Hardcoded Defaults
When hardcoding defaults, provide an override directive. Pattern from proxy: default hidden headers are hardcoded, but proxy_pass_header overrides individual ones. ssl_conf_command is the ultimate escape hatch. Don't leave admins trapped by your decisions.
Incorrect (hardcoded strip list with no way to override):
/* admin cannot pass X-Real-IP or X-Request-ID even when they need to */
static ngx_str_t mymod_hidden_headers[] = {
ngx_string("X-Real-IP"),
ngx_string("X-Request-ID"),
ngx_string("X-Internal-Trace"),
ngx_null_string
};
static ngx_int_t
ngx_http_mymodule_filter_headers(ngx_http_request_t *r)
{
ngx_uint_t i;
/* unconditionally strips headers — no config can change this */
for (i = 0; mymod_hidden_headers[i].len; i++) {
ngx_http_mymodule_remove_header(r, &mymod_hidden_headers[i]);
}
return NGX_OK;
}Correct (hardcoded defaults with per-header override directive):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* escape hatch: mymod_pass_header X-Real-IP; */
{ ngx_string("mymod_pass_header"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_http_mymodule_pass_header,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
ngx_null_command
};
static ngx_int_t
ngx_http_mymodule_filter_headers(ngx_http_request_t *r)
{
ngx_uint_t i;
ngx_http_mymodule_loc_conf_t *mlcf;
mlcf = ngx_http_get_module_loc_conf(r, ngx_http_mymodule_module);
for (i = 0; mymod_hidden_headers[i].len; i++) {
/* skip headers the admin explicitly passed through */
if (ngx_http_mymodule_is_passed(mlcf, &mymod_hidden_headers[i])) {
continue;
}
ngx_http_mymodule_remove_header(r, &mymod_hidden_headers[i]);
}
return NGX_OK;
}Use Feature Gates for Optional Behavior
Features that add overhead (caching, buffering, logging) must be gated by an on/off directive. Following nginx pattern: proxy_cache off by default, proxy_buffering on by default. The gate directive controls whether the feature's other directives have any effect.
Incorrect (cache logic always runs even when no cache is configured):
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_http_mymodule_loc_conf_t *mlcf;
mlcf = ngx_http_get_module_loc_conf(r, ngx_http_mymodule_module);
/* BUG: always runs cache lookup even when caching is unwanted —
* wastes cycles computing keys and checking empty shared zones */
ngx_http_mymodule_compute_cache_key(r, mlcf);
if (ngx_http_mymodule_cache_lookup(r, mlcf) == NGX_OK) {
return ngx_http_mymodule_send_cached(r, mlcf);
}
return ngx_http_mymodule_fetch_upstream(r, mlcf);
}Correct (gate flag checked early, all feature logic skipped when disabled):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* gate directive — mymod_cache on|off (default: off) */
{ ngx_string("mymod_cache"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, cache_enabled),
NULL },
ngx_null_command
};
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_http_mymodule_loc_conf_t *mlcf;
mlcf = ngx_http_get_module_loc_conf(r, ngx_http_mymodule_module);
/* skip all cache logic when the gate is off — zero overhead */
if (mlcf->cache_enabled) {
ngx_http_mymodule_compute_cache_key(r, mlcf);
if (ngx_http_mymodule_cache_lookup(r, mlcf) == NGX_OK) {
return ngx_http_mymodule_send_cached(r, mlcf);
}
}
return ngx_http_mymodule_fetch_upstream(r, mlcf);
}Always Expose External Resource Paths
File paths, socket paths, and external resource references MUST be directives, never hardcoded. Paths vary between environments (dev/staging/prod), containerized vs bare-metal, and different OS layouts. Following nginx pattern: ssl_certificate, fastcgi_pass unix:/path, proxy_cache_path.
Incorrect (hardcoded paths force recompilation per environment):
#define MYMOD_CERT_PATH "/etc/ssl/certs/mymodule.pem"
#define MYMOD_SOCKET_PATH "/var/run/mymod/backend.sock"
static ngx_int_t
ngx_http_mymodule_init(ngx_conf_t *cf)
{
/* containers mount certs at /certs, macOS uses /opt/homebrew,
* CI uses /tmp — none of these work without recompiling */
if (ngx_http_mymodule_load_cert(MYMOD_CERT_PATH) != NGX_OK) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"cannot load cert: %s", MYMOD_CERT_PATH);
return NGX_ERROR;
}
return NGX_OK;
}Correct (all external paths configurable via directives):
static ngx_command_t ngx_http_mymodule_commands[] = {
{ ngx_string("mymod_certificate"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
ngx_conf_set_str_slot,
NGX_HTTP_SRV_CONF_OFFSET,
offsetof(ngx_http_mymodule_srv_conf_t, certificate),
NULL },
{ ngx_string("mymod_backend"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_http_mymodule_set_backend,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
ngx_null_command
};
static char *
ngx_http_mymodule_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
{
ngx_http_mymodule_srv_conf_t *prev = parent;
ngx_http_mymodule_srv_conf_t *conf = child;
/* no default — admin must explicitly set the cert path */
ngx_conf_merge_str_value(conf->certificate, prev->certificate, "");
if (conf->certificate.len == 0) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"\"mymod_certificate\" must be set");
return NGX_CONF_ERROR;
}
return NGX_CONF_OK;
}Audit Security Implications of Every Exposed Directive
Every directive that accepts a path, URL, regex, or enables network access is a potential attack surface. Path directives enable traversal, regex directives enable ReDoS, URL directives enable SSRF. Validate at parse time, restrict what each directive can accept, and document security implications.
Incorrect (arbitrary URL accepted without scheme validation):
static char *
ngx_http_mymodule_set_endpoint(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_mymodule_loc_conf_t *mlcf = conf;
ngx_str_t *value;
value = cf->args->elts;
/* BUG: accepts file:///etc/passwd, gopher://, or any scheme —
* enables SSRF when module makes outbound requests */
mlcf->endpoint = value[1];
return NGX_CONF_OK;
}Correct (validate scheme and reject dangerous protocols at parse time):
static char *
ngx_http_mymodule_set_endpoint(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_mymodule_loc_conf_t *mlcf = conf;
ngx_str_t *value;
ngx_url_t u;
value = cf->args->elts;
/* reject non-HTTP schemes at config parse time */
if (ngx_strncasecmp(value[1].data, (u_char *) "http://", 7) != 0
&& ngx_strncasecmp(value[1].data, (u_char *) "https://", 8) != 0)
{
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"\"mymod_endpoint\" requires http:// or https:// "
"scheme, got \"%V\"", &value[1]);
return NGX_CONF_ERROR;
}
/* strip scheme before ngx_parse_url — it expects host:port/path */
ngx_memzero(&u, sizeof(ngx_url_t));
if (ngx_strncasecmp(value[1].data, (u_char *) "https://", 8) == 0) {
u.url.data = value[1].data + 8;
u.url.len = value[1].len - 8;
} else {
u.url.data = value[1].data + 7;
u.url.len = value[1].len - 7;
}
u.no_resolve = 1;
if (ngx_parse_url(cf->pool, &u) != NGX_OK) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid URL in \"mymod_endpoint\": \"%V\"",
&value[1]);
return NGX_CONF_ERROR;
}
mlcf->endpoint = value[1];
return NGX_CONF_OK;
}Avoid Over-Configuration
Not every internal constant needs a directive. If fewer than 1 in 100 deployments would change a value, hardcode it. The proxy module has ~60 directives for good reason, but most third-party modules should have 5-15. Each directive is a maintenance burden and documentation requirement.
Incorrect (exposing internal tuning knobs no admin should touch):
static ngx_command_t ngx_http_mymodule_commands[] = {
{ ngx_string("mymod_chain_link_count"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_num_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, chain_links),
NULL },
{ ngx_string("mymod_hash_bucket_size"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_num_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, bucket_size),
NULL },
{ ngx_string("mymod_retry_backoff_factor"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_num_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, backoff_factor),
NULL },
/* 20+ more internal tuning directives that no admin understands */
ngx_null_command
};Correct (hardcode internals, expose only admin-meaningful controls):
/* internal constants — no directive needed */
#define MYMOD_CHAIN_LINKS 4
#define MYMOD_HASH_BUCKET_SIZE 64
#define MYMOD_RETRY_BACKOFF 2
static ngx_command_t ngx_http_mymodule_commands[] = {
/* admins understand and need these */
{ ngx_string("mymod_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_string("mymod_max_retries"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_num_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, max_retries),
NULL },
{ ngx_string("mymod_buffer_size"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_size_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, buffer_size),
NULL },
ngx_null_command
};Mirror Nginx Core Suffix Patterns for Analogous Directives
If your module does something analogous to an existing nginx module, use the same suffix pattern. The proxy/fastcgi/uwsgi/scgi modules demonstrate this: all use _pass, _buffering, _connect_timeout, _cache, _next_upstream. An admin who knows proxy_* immediately understands mymod_* if suffixes match.
Incorrect (inventing new names for existing nginx concepts):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* BUG: "backend" instead of "pass" — nginx convention is _pass */
{ ngx_string("mymod_backend"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_http_mymodule_pass,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
/* BUG: "retry_on_failure" — nginx uses _next_upstream */
{ ngx_string("mymod_retry_on_failure"),
NGX_HTTP_LOC_CONF|NGX_CONF_1MORE,
ngx_http_mymodule_next_upstream,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
/* BUG: "backend_timeout" — nginx uses _connect_timeout */
{ ngx_string("mymod_backend_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, connect_timeout),
NULL },
ngx_null_command
};Correct (matching nginx suffix patterns for analogous functionality):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* matches proxy_pass, fastcgi_pass, uwsgi_pass */
{ ngx_string("mymod_pass"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_http_mymodule_pass,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
/* matches proxy_next_upstream, fastcgi_next_upstream */
{ ngx_string("mymod_next_upstream"),
NGX_HTTP_LOC_CONF|NGX_CONF_1MORE,
ngx_http_mymodule_next_upstream,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
/* matches proxy_connect_timeout, fastcgi_connect_timeout */
{ ngx_string("mymod_connect_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, connect_timeout),
NULL },
/* matches proxy_buffering, fastcgi_buffering */
{ ngx_string("mymod_buffering"),
NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, buffering),
NULL },
ngx_null_command
};Use Lowercase with Underscores Only
All nginx directives use lowercase_with_underscores. No camelCase, no hyphens, no UPPERCASE. This is non-negotiable. Violating this convention makes your module look like it doesn't belong in the nginx ecosystem and confuses config syntax highlighters.
Incorrect (non-standard casing breaks config parsers and admin expectations):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* BUG: camelCase — nginx config is not JavaScript */
{ ngx_string("myMod_bufferSize"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_size_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, buffer_size),
NULL },
/* BUG: hyphens — nginx uses underscores, not kebab-case */
{ ngx_string("mymod-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: UPPERCASE — screaming case has no precedent in nginx */
{ ngx_string("MYMOD_CACHE"),
NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, cache),
NULL },
ngx_null_command
};Correct (lowercase with underscores — the only accepted style):
static ngx_command_t ngx_http_mymodule_commands[] = {
{ ngx_string("mymod_buffer_size"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_size_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, buffer_size),
NULL },
{ ngx_string("mymod_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_string("mymod_cache"),
NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, cache),
NULL },
ngx_null_command
};Use a Consistent Module Prefix for All Directives
Every directive in your module MUST share the same short, clear prefix (e.g., proxy_, ssl_, fastcgi_). Core directives like sendfile have no prefix because they are foundational — your module is not foundational. The prefix acts as a namespace, preventing collisions with other modules and making your directives scannable in nginx.conf.
Incorrect (mixed prefixes create ambiguity and risk collisions):
static ngx_command_t ngx_http_mymodule_commands[] = {
{ ngx_string("mymod_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: different prefix — looks like a different module */
{ ngx_string("my_module_buffer_size"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_size_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, buffer_size),
NULL },
/* BUG: reversed prefix — impossible to find by scanning */
{ ngx_string("timeout_mymod"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_msec_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, connect_timeout),
NULL },
ngx_null_command
};Correct (consistent prefix makes all directives scannable and collision-free):
static ngx_command_t ngx_http_mymodule_commands[] = {
{ ngx_string("mymod_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_string("mymod_buffer_size"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_size_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, buffer_size),
NULL },
{ ngx_string("mymod_pass"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_http_mymodule_pass,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
ngx_null_command
};Avoid Custom Abbreviations in Directive Names
Use only universally understood abbreviations: ssl, http, tcp, uri, ip. Do not invent abbreviations for domain-specific terms. proxy_connect_timeout not proxy_conn_to. client_header_buffer_size not cli_hdr_buf_sz. Every custom abbreviation forces an admin to consult documentation.
Incorrect (custom abbreviations require documentation to decode):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* BUG: "conn_pool_sz" — three abbreviations in one name */
{ ngx_string("mymod_conn_pool_sz"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_num_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, connection_pool_size),
NULL },
/* BUG: "req_buf_cnt" — unreadable without context */
{ ngx_string("mymod_req_buf_cnt"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_num_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, request_buffer_count),
NULL },
/* BUG: "auth_tok_ttl" — is "tok" token or ticket? */
{ ngx_string("mymod_auth_tok_ttl"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_sec_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, auth_token_timeout),
NULL },
ngx_null_command
};Correct (full words are instantly readable without documentation):
static ngx_command_t ngx_http_mymodule_commands[] = {
{ ngx_string("mymod_connection_pool_size"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_num_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, connection_pool_size),
NULL },
{ ngx_string("mymod_request_buffer_count"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_num_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, request_buffer_count),
NULL },
{ ngx_string("mymod_auth_token_timeout"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_sec_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, auth_token_timeout),
NULL },
ngx_null_command
};Prefer Noun Phrases for Directive Names
Most nginx directives are noun phrases: proxy_buffer_size, client_max_body_size, keepalive_timeout. Verbs are reserved for action directives where the verb disambiguates the operation: proxy_set_header, proxy_hide_header, proxy_pass_header. Don't prefix nouns with verbs like set_ or enable_ when the noun alone is unambiguous.
Incorrect (unnecessary verbs clutter directive names):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* BUG: "set_" is redundant — all directives set values */
{ ngx_string("mymod_set_buffer_size"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_size_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, buffer_size),
NULL },
/* BUG: "enable_" is redundant — flags already imply on/off */
{ ngx_string("mymod_enable_cache"),
NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, cache),
NULL },
/* BUG: "configure_" adds noise — every directive configures */
{ ngx_string("mymod_configure_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
};Correct (clean noun phrases matching nginx core style):
static ngx_command_t ngx_http_mymodule_commands[] = {
{ ngx_string("mymod_buffer_size"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_size_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, buffer_size),
NULL },
/* on/off flag — noun is sufficient */
{ ngx_string("mymod_cache"),
NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, cache),
NULL },
{ ngx_string("mymod_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
};Group Related Directives with Sub-Prefixes
When a module has 10+ directives, group related ones with sub-prefixes. The proxy module demonstrates this: proxy_cache_* (12+ directives), proxy_ssl_* (14+ directives), proxy_cookie_* (3 directives), proxy_next_upstream* (3 directives). This creates a scannable hierarchy in documentation and config files.
Incorrect (flat namespace with no grouping — admin cannot scan related directives):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* BUG: "caching" is not a group prefix — the cache-related
* directives below use different words for the same concept */
{ ngx_string("mymod_caching"),
NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, cache_enable),
NULL },
/* BUG: "ttl" shares no prefix with "caching" — admin must know
* these are related despite having unrelated names */
{ ngx_string("mymod_ttl"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_sec_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, cache_ttl),
NULL },
/* BUG: "max_attempts" shares no prefix with other retry-related
* directives — impossible to find by prefix scanning */
{ ngx_string("mymod_max_attempts"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_num_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, retry_max),
NULL },
/* BUG: "upstream_cert" uses a different pattern from "upstream_verify"
* below — admin cannot find all TLS directives by prefix */
{ ngx_string("mymod_upstream_cert"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_str_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, ssl_certificate),
NULL },
{ ngx_string("mymod_verify"),
NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, ssl_verify),
NULL },
ngx_null_command
};Correct (grouped sub-prefixes create scannable directive families):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* mymod_cache_* group — caching directives */
{ ngx_string("mymod_cache"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_http_mymodule_cache,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
{ ngx_string("mymod_cache_valid"),
NGX_HTTP_LOC_CONF|NGX_CONF_1MORE,
ngx_http_mymodule_cache_valid,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
{ ngx_string("mymod_cache_key"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_http_mymodule_cache_key,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
/* mymod_ssl_* group — upstream TLS directives */
{ ngx_string("mymod_ssl_certificate"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_str_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, ssl_certificate),
NULL },
{ ngx_string("mymod_ssl_verify"),
NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, ssl_verify),
NULL },
ngx_null_command
};Do Not Support the if Context Unless Fully Tested
The nginx if directive creates a pseudo-location with its own configuration context. Directives inside if do NOT inherit from the parent location the way admins expect — the child context gets a fresh config that only merges from upper levels, not from the enclosing location. If your directive supports NGX_HTTP_LIF_CONF, an admin placing it inside if while putting other module directives in the parent location gets partial configuration, leading to subtle bugs. Either support if fully with extensive testing of every merge path, or omit the if-context flags entirely.
Incorrect (adding NGX_HTTP_LIF_CONF without testing inheritance inside if blocks):
static ngx_command_t ngx_http_mymodule_commands[] = {
{ ngx_string("mymod_header_filter"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF
|NGX_HTTP_LIF_CONF|NGX_CONF_TAKE1,
ngx_conf_set_str_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, header_filter),
NULL },
{ ngx_string("mymod_header_format"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF
|NGX_HTTP_LIF_CONF|NGX_CONF_TAKE1,
ngx_conf_set_str_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, header_format),
NULL },
ngx_null_command
};
/*
* BUG: admin writes this config, expecting format to inherit:
*
* location /api {
* mymod_header_format "%t %s"; # set in parent location
*
* if ($request_method = POST) {
* mymod_header_filter "x-post"; # set in if block
* # header_format is NOT inherited from parent location —
* # it gets the merge default, not "%t %s"
* }
* }
*/Correct (omitting if-context flags — nginx rejects directive inside if with a clear error):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* no NGX_HTTP_LIF_CONF — nginx will reject these inside if {} blocks
* with: "mymod_header_filter directive is not allowed here" */
{ ngx_string("mymod_header_filter"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_str_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, header_filter),
NULL },
{ ngx_string("mymod_header_format"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_str_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, header_format),
NULL },
ngx_null_command
};
/* admin sees a clear config-test error instead of a silent
* partial-config bug — they can restructure with separate
* locations instead of relying on if {} */Default to http + server + location Scope
Most operational directives (timeouts, buffer sizes, feature flags) should be configurable at all three levels: NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF. This lets admins set a global default in http {}, override per virtual host in server {}, and fine-tune per URL path in location {}. Restricting scope unnecessarily forces admins to duplicate config or accept a single inflexible value across the entire server.
Incorrect (directive only at http level — forces same value for all locations):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* BUG: NGX_HTTP_MAIN_CONF only — admin cannot override per-vhost
* or per-path, every location gets the same timeout */
{ ngx_string("mymod_timeout"),
NGX_HTTP_MAIN_CONF|NGX_CONF_TAKE1,
ngx_conf_set_msec_slot,
NGX_HTTP_MAIN_CONF_OFFSET,
offsetof(ngx_http_mymodule_main_conf_t, timeout),
NULL },
ngx_null_command
};
static void *
ngx_http_mymodule_create_main_conf(ngx_conf_t *cf)
{
ngx_http_mymodule_main_conf_t *conf;
conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_mymodule_main_conf_t));
if (conf == NULL) {
return NULL;
}
conf->timeout = NGX_CONF_UNSET_MSEC;
return conf;
}
/* no merge function — value is global, cannot vary by server or location */Correct (http + server + location with proper merge inheritance):
static ngx_command_t ngx_http_mymodule_commands[] = {
{ ngx_string("mymod_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
};
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 enables inheritance from parent levels */
conf->timeout = NGX_CONF_UNSET_MSEC;
return conf;
}
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;
/* inherits from http -> server -> location, falls back to 60s */
ngx_conf_merge_msec_value(conf->timeout, prev->timeout, 60000);
return NGX_CONF_OK;
}Restrict Shared Resource Directives to http Level Only
Directives that allocate shared memory zones, cache paths, or process-wide data structures MUST be NGX_HTTP_MAIN_CONF only. These resources are allocated once at config parse time and shared across all workers — allowing them at server or location level would create duplicate allocations or conflicting definitions. Use a separate directive at location level to select which pre-allocated zone to use, following the pattern of proxy_cache_path (http only) and proxy_cache (location level).
Incorrect (shared memory zone directive at location level — creates duplicate zones):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* BUG: allowing at LOC_CONF means each location block can try to
* create its own shared memory zone with the same name, or admins
* accidentally create many zones consuming excessive shared memory */
{ ngx_string("mymod_cache_zone"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE2,
ngx_http_mymodule_cache_zone,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
ngx_null_command
};
static char *
ngx_http_mymodule_cache_zone(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_str_t *value = cf->args->elts;
/* each location creates its own zone — duplicates if name reused,
* wastes shared memory if names differ */
ngx_shared_memory_add(cf, &value[1], ngx_atosz(value[2].data, value[2].len),
&ngx_http_mymodule_module);
return NGX_CONF_OK;
}Correct (zone creation at http only, zone selection at location level):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* define zone once at http level — allocated once, shared by all workers */
{ ngx_string("mymod_cache_zone"),
NGX_HTTP_MAIN_CONF|NGX_CONF_TAKE2,
ngx_http_mymodule_cache_zone,
NGX_HTTP_MAIN_CONF_OFFSET,
0,
NULL },
/* select which zone to use — this is safe at location level */
{ ngx_string("mymod_cache"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_http_mymodule_cache,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
ngx_null_command
};
static char *
ngx_http_mymodule_cache_zone(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_mymodule_main_conf_t *mmcf = conf;
ngx_str_t *value = cf->args->elts;
ngx_shm_zone_t *shm_zone;
/* single allocation at http level — no duplicates possible */
shm_zone = ngx_shared_memory_add(cf, &value[1],
ngx_atosz(value[2].data, value[2].len),
&ngx_http_mymodule_module);
if (shm_zone == NULL) {
return NGX_CONF_ERROR;
}
shm_zone->init = ngx_http_mymodule_init_zone;
return NGX_CONF_OK;
}
static char *
ngx_http_mymodule_cache(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_mymodule_loc_conf_t *mlcf = conf;
ngx_str_t *value = cf->args->elts;
/* location-level selection — just stores a reference to the zone */
mlcf->cache_zone = ngx_shared_memory_add(cf, &value[1], 0,
&ngx_http_mymodule_module);
if (mlcf->cache_zone == NULL) {
return NGX_CONF_ERROR;
}
return NGX_CONF_OK;
}Restrict Path-Routing Directives to Location Context
Directives that route requests to a backend (like proxy_pass, fastcgi_pass) should be restricted to NGX_HTTP_LOC_CONF|NGX_HTTP_LIF_CONF|NGX_HTTP_LMT_CONF. These directives only make sense within a matched location because the location match provides the context of which requests should be routed. Allowing them at server or http level creates ambiguous routing — every request would be forwarded with no way to serve static files or distinguish between paths.
Incorrect (pass directive at server level — ambiguous routing for all locations):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* BUG: NGX_HTTP_SRV_CONF allows this at server level where it
* would apply to ALL requests — admin cannot serve static files
* from some locations while proxying others */
{ ngx_string("mymod_pass"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_http_mymodule_pass,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
ngx_null_command
};
/*
* admin writes this, expecting selective routing but getting all-or-nothing:
*
* server {
* mymod_pass backend:8080; # every request goes to backend
*
* location /static/ {
* root /var/www; # never reached — mymod_pass already matched
* }
* }
*/Correct (restricted to location + if-in-location + limit_except — requires explicit location matching):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* location context only — admin must define which paths to route */
{ ngx_string("mymod_pass"),
NGX_HTTP_LOC_CONF|NGX_HTTP_LIF_CONF|NGX_HTTP_LMT_CONF|NGX_CONF_TAKE1,
ngx_http_mymodule_pass,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
/* tuning directives for the pass can still be at all three levels */
{ ngx_string("mymod_connect_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, connect_timeout),
NULL },
ngx_null_command
};
/*
* admin gets clear, explicit routing:
*
* server {
* mymod_connect_timeout 5s; # default for all locations
*
* location /api/ {
* mymod_pass backend:8080; # only /api/ requests are routed
* mymod_connect_timeout 2s; # override for this path
* }
*
* location /static/ {
* root /var/www; # served directly, no conflict
* }
* }
*/Use http + server Scope for Connection-Level Settings
Connection-level settings like SSL/TLS configuration happen before location matching. SSL is negotiated at connection time during the TLS handshake, not per-request — by the time nginx matches a location, the connection properties are already fixed. Allowing connection-level directives at location level is misleading because they silently have no effect, giving admins false confidence that different locations can use different TLS settings.
Incorrect (SSL-related directive at location level — silently ignored):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* BUG: NGX_HTTP_LOC_CONF allows admins to write this inside
* a location block, but the TLS handshake is already complete
* before location matching — the directive has no effect */
{ ngx_string("mymod_tls_verify_depth"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_num_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, tls_verify_depth),
NULL },
{ ngx_string("mymod_tls_protocols"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_1MORE,
ngx_conf_set_bitmask_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, tls_protocols),
&ngx_http_mymodule_tls_protocols },
ngx_null_command
};Correct (connection-level directives restricted to http + server scope):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* http + server only — matches the connection lifecycle,
* nginx rejects it inside location with a clear config error */
{ ngx_string("mymod_tls_verify_depth"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
ngx_conf_set_num_slot,
NGX_HTTP_SRV_CONF_OFFSET,
offsetof(ngx_http_mymodule_srv_conf_t, tls_verify_depth),
NULL },
{ ngx_string("mymod_tls_protocols"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_1MORE,
ngx_conf_set_bitmask_slot,
NGX_HTTP_SRV_CONF_OFFSET,
offsetof(ngx_http_mymodule_srv_conf_t, tls_protocols),
&ngx_http_mymodule_tls_protocols },
ngx_null_command
};
static void *
ngx_http_mymodule_create_srv_conf(ngx_conf_t *cf)
{
ngx_http_mymodule_srv_conf_t *conf;
conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_mymodule_srv_conf_t));
if (conf == NULL) {
return NULL;
}
conf->tls_verify_depth = NGX_CONF_UNSET;
conf->tls_protocols = 0;
return conf;
}
static char *
ngx_http_mymodule_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
{
ngx_http_mymodule_srv_conf_t *prev = parent;
ngx_http_mymodule_srv_conf_t *conf = child;
ngx_conf_merge_value(conf->tls_verify_depth, prev->tls_verify_depth, 1);
ngx_conf_merge_bitmask_value(conf->tls_protocols, prev->tls_protocols,
NGX_SSL_TLSv1_2|NGX_SSL_TLSv1_3);
return NGX_CONF_OK;
}Avoid Block Directives for Features
Block directives (NGX_CONF_BLOCK) create new configuration contexts. They are for structural elements like server { }, location { }, and upstream { } -- not for feature configuration. If you think you need a block, consider whether multi-argument directives or multiple related directives with a shared prefix would be cleaner. Block directives add context inheritance complexity, require their own create/merge functions, and confuse admins who expect feature config to be flat.
Incorrect (block directive for feature config that could be flat):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* BUG: block directive for rate limiting config — creates a nested
* context that needs its own create/merge, confuses inheritance */
{ ngx_string("mymod_rate_limit"),
NGX_HTTP_LOC_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS,
ngx_http_mymodule_rate_limit_block,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
ngx_null_command
};
/* nginx.conf — unnecessary nesting for 3 simple values */
/*
* location /api {
* mymod_rate_limit {
* rate 100;
* burst 50;
* delay nodelay;
* }
* }
*/Correct (flat directives with sub-prefix grouping keep config simple):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* flat directives — no extra context, standard inheritance */
{ ngx_string("mymod_rate"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|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 },
{ ngx_string("mymod_rate_burst"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|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_burst),
NULL },
{ ngx_string("mymod_rate_nodelay"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, rate_nodelay),
NULL },
ngx_null_command
};
/* nginx.conf — flat, scannable, inherits naturally */
/*
* location /api {
* mymod_rate 100;
* mymod_rate_burst 50;
* mymod_rate_nodelay on;
* }
*/Use Custom Handlers for Complex Directive Parsing
Built-in slot functions handle simple single-value directives. When a directive needs multiple arguments with different types, key=value syntax, cross-field validation, or conditional logic, use a custom set handler. The handler receives cf->args and can parse, validate, and set multiple config fields from a single directive -- keeping the admin-facing syntax clean while handling internal complexity.
Note: For simple two-argument key-value directives (like proxy_set_header Host $host), use the built-in ngx_conf_set_keyval_slot which stores pairs into an ngx_array_t of ngx_keyval_t. The custom handler below is for complex cases with key=value syntax, multiple parameter types, or cross-field validation.
Incorrect (forcing a built-in slot for a directive with key=value pairs):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* BUG: str_slot can only store the first argument as a string —
* "mymod_limit_req 100r/s burst=50" loses the burst parameter */
{ ngx_string("mymod_limit_req"),
NGX_HTTP_LOC_CONF|NGX_CONF_1MORE,
ngx_conf_set_str_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, limit_req_str),
NULL },
ngx_null_command
};
/* the built-in slot silently stores only "100r/s",
* burst=50 and nodelay are ignored with no error */Correct (custom handler parses positional and key=value arguments with validation):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* custom handler for: mymod_limit_req rate=100r/s burst=50 nodelay */
{ ngx_string("mymod_limit_req"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_1MORE,
ngx_http_mymodule_limit_req,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
ngx_null_command
};
static char *
ngx_http_mymodule_limit_req(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_mymodule_loc_conf_t *mlcf = conf;
ngx_str_t *value;
ngx_uint_t i;
ngx_int_t rate, burst;
value = cf->args->elts;
rate = 0;
burst = 0;
mlcf->limit_nodelay = 0;
for (i = 1; i < cf->args->nelts; i++) {
if (ngx_strncmp(value[i].data, "rate=", 5) == 0) {
/* parse the numeric part after "rate=" */
rate = ngx_atoi(value[i].data + 5, value[i].len - 5);
if (rate <= 0) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid rate \"%V\" in "
"\"mymod_limit_req\" directive",
&value[i]);
return NGX_CONF_ERROR;
}
mlcf->limit_rate = rate;
continue;
}
/* ... similar parsing for "burst=", "nodelay" ... */
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid parameter \"%V\" in "
"\"mymod_limit_req\" directive", &value[i]);
return NGX_CONF_ERROR;
}
/* cross-field validation — burst without rate makes no sense */
if (mlcf->limit_rate == 0) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"\"mymod_limit_req\" requires \"rate\" parameter");
return NGX_CONF_ERROR;
}
return NGX_CONF_OK;
}Use Enum Slot for Known Value Sets
When a directive accepts one of a known set of values (like ssl_verify_client on|off|optional|optional_no_ca), use ngx_conf_set_enum_slot with an ngx_conf_enum_t array. This validates the value at parse time and gives a clear error on typos. Using a string slot accepts anything and defers validation to request time, or worse, never validates at all.
Incorrect (string slot accepts typos and silently falls through):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* BUG: string slot — "agressive" typo silently accepted */
{ ngx_string("mymod_cache_strategy"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_str_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, cache_strategy),
NULL },
ngx_null_command
};
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_http_mymodule_loc_conf_t *mlcf;
mlcf = ngx_http_get_module_loc_conf(r, ngx_http_mymodule_module);
/* strcmp chain on every request — typo "agressive" matches nothing,
* silently falls to default behavior with no warning */
if (ngx_strcmp(mlcf->cache_strategy.data, "aggressive") == 0) {
return ngx_http_mymodule_cache_aggressive(r);
} else if (ngx_strcmp(mlcf->cache_strategy.data, "conservative") == 0) {
return ngx_http_mymodule_cache_conservative(r);
} else if (ngx_strcmp(mlcf->cache_strategy.data, "off") == 0) {
return NGX_DECLINED;
}
/* typos land here — no error, just wrong behavior */
return ngx_http_mymodule_cache_conservative(r);
}Correct (enum slot validates at parse time and stores an integer constant):
#define NGX_HTTP_MYMOD_CACHE_OFF 0
#define NGX_HTTP_MYMOD_CACHE_CONSERVATIVE 1
#define NGX_HTTP_MYMOD_CACHE_AGGRESSIVE 2
static ngx_conf_enum_t ngx_http_mymodule_cache_strategies[] = {
{ ngx_string("off"), NGX_HTTP_MYMOD_CACHE_OFF },
{ ngx_string("conservative"), NGX_HTTP_MYMOD_CACHE_CONSERVATIVE },
{ ngx_string("aggressive"), NGX_HTTP_MYMOD_CACHE_AGGRESSIVE },
{ ngx_null_string, 0 }
};
static ngx_command_t ngx_http_mymodule_commands[] = {
/* parse-time validation — "agressive" typo rejected immediately */
{ ngx_string("mymod_cache_strategy"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_enum_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, cache_strategy),
&ngx_http_mymodule_cache_strategies },
ngx_null_command
};
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_http_mymodule_loc_conf_t *mlcf;
mlcf = ngx_http_get_module_loc_conf(r, ngx_http_mymodule_module);
/* integer comparison — fast, no typo risk, exhaustive switch */
switch (mlcf->cache_strategy) {
case NGX_HTTP_MYMOD_CACHE_AGGRESSIVE:
return ngx_http_mymodule_cache_aggressive(r);
case NGX_HTTP_MYMOD_CACHE_CONSERVATIVE:
return ngx_http_mymodule_cache_conservative(r);
case NGX_HTTP_MYMOD_CACHE_OFF:
return NGX_DECLINED;
}
return NGX_DECLINED;
}Use NGX_CONF_FLAG for Binary Toggles
When a directive is a binary toggle, use NGX_CONF_FLAG with ngx_conf_set_flag_slot. Nginx admins expect on|off syntax for feature switches. Name the directive as the feature noun (mymod_buffering, ssl_stapling), not mymod_enable_buffering. The flag pattern automatically rejects any value that is not on or off.
Incorrect (string slot accepts arbitrary values and defers validation):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* BUG: TAKE1 with string slot — accepts "true", "yes", "1", anything */
{ ngx_string("mymod_buffering"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_str_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, buffering_str),
NULL },
ngx_null_command
};
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_http_mymodule_loc_conf_t *mlcf;
mlcf = ngx_http_get_module_loc_conf(r, ngx_http_mymodule_module);
/* must strcmp every request — "true"/"false"/"1"/"0" all need handling,
* typos like "ture" silently treated as disabled */
if (ngx_strcmp(mlcf->buffering_str.data, "true") == 0
|| ngx_strcmp(mlcf->buffering_str.data, "1") == 0)
{
return ngx_http_mymodule_buffered(r, mlcf);
}
return ngx_http_mymodule_unbuffered(r, mlcf);
}Correct (NGX_CONF_FLAG validates at parse time and stores an integer):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* on|off only — nginx rejects anything else at config-test time */
{ ngx_string("mymod_buffering"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, buffering),
NULL },
ngx_null_command
};
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;
}
conf->buffering = NGX_CONF_UNSET;
return conf;
}
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_value(conf->buffering, prev->buffering, 1);
return NGX_CONF_OK;
}Use 1MORE for Variable-Length Value Lists
When a directive accepts a variable-length list of values (like ssl_protocols TLSv1.2 TLSv1.3 or proxy_next_upstream error timeout http_502 http_503), use NGX_CONF_1MORE. This accepts one or more arguments and delegates to a custom handler that iterates cf->args. Use this pattern for bitmask flags, protocol lists, and error condition lists where the admin selects a subset from a known set.
Note: For simple bitmask cases where each argument maps directly to a flag without extra logic (like the off keyword or custom validation), use the built-in ngx_conf_set_bitmask_slot with an ngx_conf_bitmask_t array instead of a custom handler. The custom handler shown below is needed when the directive has special keywords, conditional logic, or validation beyond simple flag OR-ing.
Incorrect (separate directives for each value in the set):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* BUG: one directive per retry condition — verbose, hard to maintain,
* admin must write 4 lines instead of 1 */
{ ngx_string("mymod_retry_on_error"),
NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, retry_error),
NULL },
{ ngx_string("mymod_retry_on_timeout"),
NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, retry_timeout),
NULL },
{ ngx_string("mymod_retry_on_500"),
NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, retry_500),
NULL },
{ ngx_string("mymod_retry_on_502"),
NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, retry_502),
NULL },
ngx_null_command
};
/* nginx.conf — 4 lines for what should be 1 */
/* mymod_retry_on_error on; */
/* mymod_retry_on_timeout on; */
/* mymod_retry_on_500 on; */
/* mymod_retry_on_502 on; */Correct (single directive with 1MORE accumulates a bitmask from all arguments):
#define NGX_HTTP_MYMOD_RETRY_ERROR 0x0002
#define NGX_HTTP_MYMOD_RETRY_TIMEOUT 0x0004
#define NGX_HTTP_MYMOD_RETRY_500 0x0008
#define NGX_HTTP_MYMOD_RETRY_502 0x0010
#define NGX_HTTP_MYMOD_RETRY_503 0x0020
#define NGX_HTTP_MYMOD_RETRY_504 0x0040
#define NGX_HTTP_MYMOD_RETRY_OFF 0x0001
static ngx_command_t ngx_http_mymodule_commands[] = {
/* admin writes: mymod_retry error timeout http_502 http_503; */
{ ngx_string("mymod_retry"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_1MORE,
ngx_http_mymodule_retry,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
ngx_null_command
};
static char *
ngx_http_mymodule_retry(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_mymodule_loc_conf_t *mlcf = conf;
ngx_str_t *value;
ngx_uint_t i;
value = cf->args->elts;
for (i = 1; i < cf->args->nelts; i++) {
if (ngx_strcmp(value[i].data, "off") == 0) {
mlcf->retry_mask = NGX_HTTP_MYMOD_RETRY_OFF;
return NGX_CONF_OK;
}
if (ngx_strcmp(value[i].data, "error") == 0) {
mlcf->retry_mask |= NGX_HTTP_MYMOD_RETRY_ERROR;
continue;
}
if (ngx_strcmp(value[i].data, "timeout") == 0) {
mlcf->retry_mask |= NGX_HTTP_MYMOD_RETRY_TIMEOUT;
continue;
}
/* ... similar for "http_500", "http_502", "http_503", "http_504" ... */
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid retry condition \"%V\" in "
"\"mymod_retry\" directive", &value[i]);
return NGX_CONF_ERROR;
}
return NGX_CONF_OK;
}Use TAKE1/TAKE2/TAKE12 for Fixed Argument Counts
Use NGX_CONF_TAKE1 for single-value directives, NGX_CONF_TAKE2 for exactly two arguments (like proxy_buffers 8 4k), and NGX_CONF_TAKE12 or NGX_CONF_TAKE123 for optional second or third arguments (like keepalive_timeout 75s or keepalive_timeout 75s 60s). Never use NGX_CONF_1MORE when the argument count is actually fixed -- it prevents nginx from catching wrong argument counts at config-test time.
Incorrect (1MORE accepts any argument count, including wrong ones):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* BUG: 1MORE allows "mymod_buffers 8" or "mymod_buffers 8 4k extra junk"
* — both parse without error, handler must validate manually */
{ ngx_string("mymod_buffers"),
NGX_HTTP_LOC_CONF|NGX_CONF_1MORE,
ngx_http_mymodule_buffers,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
ngx_null_command
};
static char *
ngx_http_mymodule_buffers(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_mymodule_loc_conf_t *mlcf = conf;
ngx_str_t *value;
value = cf->args->elts;
/* must manually check argument count — should have been nginx's job */
if (cf->args->nelts != 3) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid number of arguments in "
"\"mymod_buffers\" directive");
return NGX_CONF_ERROR;
}
mlcf->bufs.num = ngx_atoi(value[1].data, value[1].len);
mlcf->bufs.size = ngx_parse_size(&value[2]);
return NGX_CONF_OK;
}Correct (TAKE2 enforces exactly two arguments at parse time):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* exactly 2 args: mymod_buffers <number> <size>
* nginx rejects wrong argument count before handler runs */
{ ngx_string("mymod_buffers"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE2,
ngx_http_mymodule_buffers,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
/* 1 or 2 args: mymod_keepalive <timeout> [header_timeout]
* matches keepalive_timeout pattern — second arg is optional */
{ ngx_string("mymod_keepalive"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE12,
ngx_http_mymodule_keepalive,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
ngx_null_command
};
static char *
ngx_http_mymodule_buffers(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_mymodule_loc_conf_t *mlcf = conf;
ngx_str_t *value;
value = cf->args->elts;
/* cf->args->nelts is guaranteed to be 3 (directive name + 2 args) */
mlcf->bufs.num = ngx_atoi(value[1].data, value[1].len);
if (mlcf->bufs.num == NGX_ERROR || mlcf->bufs.num == 0) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid number of buffers in "
"\"mymod_buffers\" directive");
return NGX_CONF_ERROR;
}
mlcf->bufs.size = ngx_parse_size(&value[2]);
if (mlcf->bufs.size == (size_t) NGX_ERROR || mlcf->bufs.size == 0) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid buffer size in "
"\"mymod_buffers\" directive");
return NGX_CONF_ERROR;
}
return NGX_CONF_OK;
}Use Time and Size Slot Functions for Time and Size Values
Always use ngx_conf_set_msec_slot for timeouts and ngx_conf_set_size_slot for buffer and limit sizes. Nginx admins expect to write 60s, 5m, 8k, 1m -- not 60000, 300000, 8192, 1048576. Using ngx_conf_set_num_slot for time or size values violates admin expectations and creates unit confusion that leads to outages.
Incorrect (raw number slot forces admins to calculate milliseconds manually):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* BUG: num_slot — admin must write 60000 instead of 60s,
* easy to confuse seconds vs milliseconds */
{ ngx_string("mymod_timeout"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_num_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, timeout),
NULL },
/* BUG: num_slot — admin must write 8192 instead of 8k,
* is 1000 bytes or 1024? nobody knows */
{ ngx_string("mymod_buffer_size"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_num_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, buffer_size),
NULL },
ngx_null_command
};
/* nginx.conf — what unit is 30000? milliseconds? microseconds? seconds? */
/* mymod_timeout 30000; */
/* mymod_buffer_size 8192; */Correct (msec and size slots accept human-readable units):
static ngx_command_t ngx_http_mymodule_commands[] = {
/* admin writes: mymod_timeout 30s; or mymod_timeout 2m; */
{ ngx_string("mymod_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 },
/* admin writes: mymod_buffer_size 8k; or mymod_buffer_size 1m; */
{ ngx_string("mymod_buffer_size"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_size_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mymodule_loc_conf_t, buffer_size),
NULL },
ngx_null_command
};
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;
}
conf->timeout = NGX_CONF_UNSET_MSEC;
conf->buffer_size = NGX_CONF_UNSET_SIZE;
return conf;
}
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_size_value(conf->buffer_size, prev->buffer_size, 8192);
return NGX_CONF_OK;
}Provide Actionable Guidance in Error Messages
Error messages should tell the admin HOW to fix the problem, not just state it. Include the expected format, required prefix, or correct syntax. Following nginx core pattern: "host not found in upstream" tells what's wrong; a good module adds what the correct format looks like so the admin can fix the config without searching documentation.
Incorrect (states the problem without any hint about the expected format):
static char *
ngx_http_mymodule_set_backend(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_mymodule_loc_conf_t *mlcf = conf;
ngx_str_t *value;
ngx_url_t u;
value = cf->args->elts;
ngx_memzero(&u, sizeof(ngx_url_t));
u.url = value[1];
u.no_resolve = 1;
if (ngx_parse_url(cf->pool, &u) != NGX_OK) {
/* BUG: admin sees "invalid URL in mymod_backend" but has no idea
* whether the problem is a missing scheme, port, path, or something
* else — must search docs or source code to figure out the format */
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid URL in \"%V\" directive",
&cmd->name);
return NGX_CONF_ERROR;
}
mlcf->backend = u;
return NGX_CONF_OK;
}Correct (error tells the admin exactly what format is required and how to fix it):
static char *
ngx_http_mymodule_set_backend(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_mymodule_loc_conf_t *mlcf = conf;
ngx_str_t *value;
ngx_url_t u;
value = cf->args->elts;
/* check for required scheme prefix before parsing */
if (ngx_strncasecmp(value[1].data, (u_char *) "http://", 7) != 0
&& ngx_strncasecmp(value[1].data, (u_char *) "https://", 8) != 0)
{
/* actionable message: shows what's wrong AND how to fix it —
* admin sees: 'mymod_backend requires scheme prefix,
* got "backend.local" — use "http://backend.local"' */
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"\"%V\" requires scheme prefix, got \"%V\" "
"— use \"http://%V\" or \"https://%V\"",
&cmd->name, &value[1], &value[1], &value[1]);
return NGX_CONF_ERROR;
}
ngx_memzero(&u, sizeof(ngx_url_t));
u.url = value[1];
u.no_resolve = 1;
if (ngx_parse_url(cf->pool, &u) != NGX_OK) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"\"%V\" has invalid URL \"%V\": %s",
&cmd->name, &value[1], u.err);
return NGX_CONF_ERROR;
}
mlcf->backend = u;
return NGX_CONF_OK;
}Detect Conflicting Directives at Merge Time
Some directives are mutually exclusive (e.g., you cannot enable both caching and pass-through). Detect these conflicts in merge_loc_conf and emit a clear error. If left undetected, the runtime behavior depends on which code path runs first — a race condition in configuration that produces intermittent, hard-to-debug failures.
Incorrect (silently accepts conflicting directives — undefined runtime behavior):
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_value(conf->cache, prev->cache, 0);
ngx_conf_merge_value(conf->pass_through, prev->pass_through, 0);
/* BUG: both cache and pass_through can be "on" at the same time —
* handler checks cache first on some requests, pass_through first
* on others depending on internal state, producing inconsistent
* behavior that is nearly impossible to reproduce */
ngx_conf_merge_msec_value(conf->timeout, prev->timeout, 60000);
return NGX_CONF_OK;
}Correct (detects the conflict at merge time — admin sees clear error during nginx -t):
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_value(conf->cache, prev->cache, 0);
ngx_conf_merge_value(conf->pass_through, prev->pass_through, 0);
/* detect mutually exclusive directives before any request is served */
if (conf->cache && conf->pass_through) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"\"mymod_cache\" and \"mymod_pass_through\" are "
"mutually exclusive — disable one of them");
return NGX_CONF_ERROR;
}
ngx_conf_merge_msec_value(conf->timeout, prev->timeout, 60000);
return NGX_CONF_OK;
}Validate All Directive Values at Config Parse Time
Every directive value must be fully validated in the set handler or merge function, never deferred to request processing. The admin runs nginx -t to test config before reload — if validation happens at request time, bad config passes the test and breaks live traffic. Parse-time validation catches the problem before any request is served.
Incorrect (stores raw string, validates on first request — bad config passes nginx -t):
static char *
ngx_http_mymodule_set_timeout(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_mymodule_loc_conf_t *mlcf = conf;
ngx_str_t *value;
value = cf->args->elts;
/* BUG: stores the raw string without validation — "nginx -t" succeeds
* even if the value is "banana", failure happens on first request */
mlcf->timeout_raw = value[1];
return NGX_CONF_OK;
}
static ngx_int_t
ngx_http_mymodule_handler(ngx_http_request_t *r)
{
ngx_http_mymodule_loc_conf_t *mlcf;
ngx_int_t ms;
mlcf = ngx_http_get_module_loc_conf(r, ngx_http_mymodule_module);
/* validation at request time — every request pays the cost,
* and the first user to hit this location gets a 500 */
ms = ngx_parse_time(&mlcf->timeout_raw, 0);
if (ms == NGX_ERROR) {
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
"invalid timeout value");
return NGX_HTTP_INTERNAL_SERVER_ERROR;
}
/* ... */
return NGX_OK;
}Correct (validates in set handler — bad config fails nginx -t before any reload):
static char *
ngx_http_mymodule_set_timeout(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_mymodule_loc_conf_t *mlcf = conf;
ngx_str_t *value;
ngx_msec_t ms;
value = cf->args->elts;
/* validate immediately — admin sees the error during "nginx -t" */
ms = ngx_parse_time(&value[1], 0);
if (ms == (ngx_msec_t) NGX_ERROR) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid timeout value \"%V\" in \"%V\" directive",
&value[1], &cmd->name);
return NGX_CONF_ERROR;
}
mlcf->timeout = ms;
return NGX_CONF_OK;
}Include the Invalid Value in Error Messages
Error messages from ngx_conf_log_error must include the actual invalid value using the %V format specifier for ngx_str_t. Nginx automatically adds the file path and line number, but only your handler knows which specific value was wrong. Without the value, the admin must guess which part of a multi-argument directive failed.
Incorrect (error message hides the invalid value — admin must re-read the config and guess):
static char *
ngx_http_mymodule_set_timeout(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_mymodule_loc_conf_t *mlcf = conf;
ngx_str_t *value;
ngx_msec_t ms;
value = cf->args->elts;
ms = ngx_parse_time(&value[1], 0);
if (ms == (ngx_msec_t) NGX_ERROR) {
/* BUG: says which directive failed but not which value —
* if the line is "mymod_timeout abc", admin sees
* "invalid value in mymod_timeout" and must open the file to check */
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid value in \"%V\" directive",
&cmd->name);
return NGX_CONF_ERROR;
}
mlcf->timeout = ms;
return NGX_CONF_OK;
}Correct (error message includes the bad value — admin knows exactly what to fix):
static char *
ngx_http_mymodule_set_timeout(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_mymodule_loc_conf_t *mlcf = conf;
ngx_str_t *value;
ngx_msec_t ms;
value = cf->args->elts;
ms = ngx_parse_time(&value[1], 0);
if (ms == (ngx_msec_t) NGX_ERROR) {
/* includes the exact invalid value via %V — admin sees
* 'invalid timeout value "abc" in "mymod_timeout" directive' */
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid timeout value \"%V\" in \"%V\" directive",
&value[1], &cmd->name);
return NGX_CONF_ERROR;
}
mlcf->timeout = ms;
return NGX_CONF_OK;
}Include Valid Range or Format in Error Messages
When rejecting a value, tell the admin what IS valid, not just what was wrong. Include the expected range, format, or list of valid options. The admin should be able to fix the error on the first attempt without consulting documentation.
Incorrect (reports invalid value without indicating the valid range — admin must read docs):
static char *
ngx_http_mymodule_set_max_retries(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 n;
value = cf->args->elts;
n = ngx_atoi(value[1].data, value[1].len);
if (n == NGX_ERROR || n < 0 || n > 100) {
/* BUG: says value is invalid but not why or what is valid —
* admin has no idea if 500 is too high, negative, or wrong type */
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid value \"%V\" in \"%V\" directive",
&value[1], &cmd->name);
return NGX_CONF_ERROR;
}
mlcf->max_retries = (ngx_uint_t) n;
return NGX_CONF_OK;
}Correct (error states the valid range — admin fixes it on the first attempt):
static char *
ngx_http_mymodule_set_max_retries(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 n;
value = cf->args->elts;
n = ngx_atoi(value[1].data, value[1].len);
if (n == NGX_ERROR) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"\"%V\" directive requires a numeric value, "
"got \"%V\"",
&cmd->name, &value[1]);
return NGX_CONF_ERROR;
}
if (n < 0 || n > 100) {
/* includes the valid range and the actual value —
* admin sees: 'mymod_max_retries must be between 0 and 100, got 500' */
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"\"%V\" must be between 0 and 100, got %i",
&cmd->name, n);
return NGX_CONF_ERROR;
}
mlcf->max_retries = (ngx_uint_t) n;
return NGX_CONF_OK;
}Use Dynamic Prefix Variables for Key-Value Data
When your module handles key-value data (headers, metadata, tags), use the dynamic prefix pattern: $mymod_meta_<name> where <name> is user-supplied at config time. Pattern from nginx core: $http_<name> (request headers), $upstream_http_<name> (upstream response headers), $cookie_<name>, $arg_<name>. Register a prefix variable handler that looks up the suffix dynamically at runtime.
Incorrect (pre-registering individual variables for each possible key):
static ngx_http_variable_t ngx_http_mymodule_vars[] = {
/* BUG: cannot anticipate every metadata key the backend sends —
* adding a new key requires code change and recompilation */
{ ngx_string("mymod_meta_request_id"), NULL,
ngx_http_mymodule_meta_request_id, 0, 0, 0 },
{ ngx_string("mymod_meta_trace_id"), NULL,
ngx_http_mymodule_meta_trace_id, 0, 0, 0 },
{ ngx_string("mymod_meta_region"), NULL,
ngx_http_mymodule_meta_region, 0, 0, 0 },
/* every new key = new handler function + recompile + reload */
ngx_http_null_variable
};Correct (single prefix handler resolves any $mymod_meta_<key> dynamically):
static ngx_int_t
ngx_http_mymodule_meta_variable(ngx_http_request_t *r,
ngx_http_variable_value_t *v, uintptr_t data)
{
ngx_str_t *name = (ngx_str_t *) data;
ngx_http_mymodule_ctx_t *ctx;
ngx_str_t key, value;
ngx_uint_t i;
ctx = ngx_http_get_module_ctx(r, ngx_http_mymodule_module);
if (ctx == NULL || ctx->metadata == NULL) {
v->not_found = 1;
return NGX_OK;
}
/* extract the suffix after "mymod_meta_" to use as lookup key */
key.len = name->len - (sizeof("mymod_meta_") - 1);
key.data = name->data + (sizeof("mymod_meta_") - 1);
/* convert underscores to dashes to match header convention,
* then look up the key in module metadata */
if (ngx_http_mymodule_lookup_meta(ctx->metadata, &key, &value) == NGX_OK) {
v->len = value.len;
v->data = value.data;
v->valid = 1;
v->no_cacheable = 0;
v->not_found = 0;
} else {
v->not_found = 1;
}
return NGX_OK;
}
static ngx_int_t
ngx_http_mymodule_add_variables(ngx_conf_t *cf)
{
ngx_http_variable_t *var;
static ngx_str_t prefix = ngx_string("mymod_meta_");
/* register prefix — nginx calls our handler for any $mymod_meta_<key>,
* setting data = &v[i].name during init so the handler receives
* the full variable name via the data parameter */
var = ngx_http_add_variable(cf, &prefix, NGX_HTTP_VAR_PREFIX);
if (var == NULL) {
return NGX_ERROR;
}
var->get_handler = ngx_http_mymodule_meta_variable;
return NGX_OK;
}
/* admin can now use any key without module changes:
*
* proxy_set_header X-Request-ID $mymod_meta_request_id;
* log_format meta '$mymod_meta_trace_id $mymod_meta_region';
*/Name Variables with Module Prefix and Descriptive Suffix
Variable names follow the pattern $<module_prefix>_<descriptive_name>. Pattern from nginx core: $upstream_addr, $upstream_response_time, $ssl_client_s_dn, $proxy_host. The module prefix prevents collisions with other modules and nginx core variables. The descriptive suffix makes the variable self-documenting in log_format and map blocks.
Incorrect (generic names collide with other modules, cryptic abbreviations are unreadable):
static ngx_http_variable_t ngx_http_mymodule_vars[] = {
/* BUG: "response_time" has no module prefix — collides with
* upstream, any other module, or future nginx core variables */
{ ngx_string("response_time"), NULL,
ngx_http_mymodule_response_time_variable, 0, 0, 0 },
/* BUG: "mt" is a cryptic abbreviation — admin reading
* log_format "$mt" has no idea what module this belongs to */
{ ngx_string("mt"), NULL,
ngx_http_mymodule_type_variable, 0, 0, 0 },
/* BUG: "cs" means nothing without context — cache_status?
* connection_state? content_size? */
{ ngx_string("cs"), NULL,
ngx_http_mymodule_cache_status_variable, 0, 0, 0 },
ngx_http_null_variable
};Correct (module prefix prevents collisions, descriptive suffix is self-documenting):
static ngx_http_variable_t ngx_http_mymodule_vars[] = {
/* $mymod_response_time — clear module ownership, describes the value */
{ ngx_string("mymod_response_time"), NULL,
ngx_http_mymodule_response_time_variable, 0, 0, 0 },
/* $mymod_cache_status — instantly scannable in log_format lines */
{ ngx_string("mymod_cache_status"), NULL,
ngx_http_mymodule_cache_status_variable, 0, 0, 0 },
/* $mymod_backend_addr — follows upstream_addr pattern, module-scoped */
{ ngx_string("mymod_backend_addr"), NULL,
ngx_http_mymodule_backend_addr_variable, 0, 0, 0 },
ngx_http_null_variable
};
/* usage in nginx.conf is immediately clear:
*
* log_format mymod '$remote_addr - $mymod_cache_status '
* '$mymod_response_time $mymod_backend_addr';
*/Related skills
FAQ
What does nginx-c-module-design do?
nginx-c-module-design: A skill for development. This provides functionality for development workflows.
When should I use nginx-c-module-design?
When you need to use nginx-c-module-design for development tasks, or when nginx-c-module-design: a skill for development. this provides functionality for development workflows.
What are the main capabilities?
nginx-c-module-design.