NGINX 1.31.5: Control API, predicate locations, early body inspection, and more

by

in , ,

For over two decades, NGINX has been the workhorse of the modern web. Its classic request-processing architecture—reading HTTP headers, matching a URI location path, and streaming payload data to backend services—built the foundation of modern web traffic management.

However, the way web applications and AI agent systems communicate is fundamentally evolving. Modern application stacks rely heavily on APIs and emerging agentic standards like the Model Context Protocol (MCP), alongside single-page application routers and high-volume AI web crawlers. These systems routinely send wildly different operational requests over identical, generic URIs like /api/v1, /graphql, or /mcp.

NGINX 1.31.5 brings a major leap forward in how NGINX evaluates and routes traffic, with four powerful capabilities landing directly in open-source core:

  1. Predicate variables in location blocks
  2. Early request body inspection (client_body_early_read)
  3. Native JSON parsing and variable extraction
  4. NGINX Control API (open sourced from F5 NGINX Plus)

Rethinking how NGINX routes traffic

To understand why this update is such a significant paradigm shift, think of NGINX processing HTTP requests much like how a post office processes mail:

  • The URI (/api/v1/checkout): The street address printed on the outside of the envelope.
  • The headers (Authorization, User-Agent): The postmarks, return address, and shipping class stamps affixed to the envelope.
  • The request body (including JSON payload): The actual letter sealed securely inside the envelope.

Traditional NGINX routing

Historically, NGINX acted like an exceptionally fast postal sorting machine. It sorted incoming letters strictly by reading the street address (URI) and stamps (Headers) on the outside of the envelope. It selected the delivery destination (location block) based on that outer metadata, and only after routing the envelope to the destination would the letter inside actually be opened and read.

With client_body_early_read combined with predicate variable evaluation, NGINX can now safely open the envelope, inspect the contents of the letter inside, check the payload, and evaluate both inner letter details and outer stamps before making a routing decision. This allows NGINX to route the package directly to a specialized handling department before deciding which delivery truck (upstream service) receives it.

1. Predicate locations: variable-based routing

What’s new: Any variable becomes a predicate when it’s used in a location definition (nginx/nginx#1633). This simple yet fundamental change means location matching is no longer restricted to URI path strings (like location /api). Instead, you can now route locations based on anything—from Layer 4 (L4) network subnets, client certificates, and Layer 7 (L7) request headers to custom business logic and extracted payload parameters.

The key architectural innovation in NGINX 1.31.5 is that location directives accept these evaluated variables directly:

location $is_ai_scraper {
    # Dedicated handling when $is_ai_scraper evaluates to true/non-empty
}

Why it matters: Instead of restricting location matching strictly to URI paths (like location /api), NGINX can now match location blocks against evaluated variables representing arbitrary, multi-variable logical conditions—all executing at native C-level speed with zero scripting runtime overhead.

2. Early request body reading (client_body_early_read)

What’s new: By default, NGINX reads request headers, selects a location block, and then buffers the request body payload.

Enabling client_body_early_read changes this processing sequence by forcing NGINX to buffer and parse the request payload before location matching takes place (nginx/nginx#1641).

Why it matters: Previously, routing or logging individual tool calls in AI agent architectures required njs modules like the NGINX MCP Observability Module (njs). With client_body_early_read and predicate locations, NGINX can now inspect JSON payloads, extract method signatures, and execute payload-direct routing natively in C with zero runtime overhead.

  • Payload-direct routing: Route requests to microservices based on specific JSON payload keys or MCP tool calls (tools/call) rather than generic endpoints like /mcp.
  • Shift-left edge security: Validate, rate-limit, or reject malformed or oversized payloads at the proxy layer before streaming payload data to backend services.

3. Native JSON parsing and variable extraction

What’s new: One of the most requested features in modern API gateway topologies is the ability to inspect structured JSON payloads directly within the core engine without relying on external scripting environments like Lua or njs.

NGINX 1.31.5 introduces a native JSON parser module (nginx/nginx#1642). NGINX can parse buffered JSON payloads and assign extracted JSON fields directly to NGINX variables:

  • Zero-scripting payload extraction: Parse keys, nested properties, or method fields directly out of incoming JSON payloads into standard NGINX variables.
  • Usage with early body reading: When paired with client_body_early_read, JSON variables are populated before location evaluation occurs.
  • Variable-as-predicate location: Once extracted into a variable, that JSON field can be fed into a map directive or used directly as a location predicate (location $extracted_json_method).
# Extract the JSON "method" field from buffered POST requests into $json_method
json_set $request_body $json_method "method";

Why it matters: Operators can inspect, validate, and route on API payloads directly at the edge delivering native NGINX performance (zero scripting overhead) and integration into standard NGINX configuration directives.

4. Control API: instant operational feedback

What’s new: First introduced in the NGINX Plus R37.0 long-term support (LTS) release, the Control API has now come to NGINX Open Source. It provides programmatic runtime inspection (/1/control/processes, /1/control/config) and structured configuration reloads with immediate JSON feedback (nginx/nginx#1626).

To use the API securely, launch NGINX bound to a UNIX-domain socket:

sudo nginx -l unix:/tmp/nginx.sock

Note: while Control API supports running on a network port, we strongly discourage that for security reasons. The API provides an open unauthenticated interface to NGINX internals and should be bound to a secured filesystem accessible to privileged users only.

Then trigger a configuration reload and receive instant execution status directly in the HTTP response:

curl --unix-socket /tmp/nginx.sock -X PATCH http://localhost/1/control/config

Why it matters: Historically, updating NGINX configuration required running nginx -s reload or sending UNIX signals, leaving operators to tail /var/log/nginx/error.log to catch downstream operational errors (such as configuration typos or socket binding permissions). The Control API replaces this workflow with direct, synchronous JSON feedback for automated deployment pipelines and infrastructure-as-code tools.

Bug fixes and stability enhancements

Alongside major features, NGINX 1.31.5 includes continuous maintenance improvements.

Long FastCGI and uWSGI parameter names

What’s new: A fastcgi_param name of 128 bytes or more, or a uwsgi_param name of 256 bytes or more, is now encoded with the correct length on the wire. The per-parameter length field was previously truncated to a single byte while the name itself was still copied in full, so the request that reached the upstream was malformed. FastCGI now emits the four-byte length form and uWSGI the 16-bit form, matching what both modules already did on the request header path. The 127-byte boundary for FastCGI’s one-byte form is unchanged, and SCGI was never affected because netstrings carry no fixed-width per-parameter length field (nginx/nginx#1648).

Why it matters: Names that long are rare, so this was an implementation limit rather than something most configurations meet. Where it did apply, the failure was total rather than subtle: real backends reject the malformed request—PHP-CGI resets the connection and uWSGI closes it—so NGINX answered 502 for every request to that location. Parameter names now encode correctly up to the length each protocol allows.

Stricter checks on out-of-range and out-of-sequence input

What’s new: Three validation fixes reject input where it’s validated, rather than carrying it into arithmetic or a state change that can’t hold it:

  • Memcached response lengths are rejected near the maximum: A VALUE line declaring a length within seven bytes of NGX_MAX_OFF_T_VALUE overflowed once the module added the size of the \r\nEND\r\n trailer, so the trailer was never validated and the length counter underflowed. Those lengths are now rejected where other invalid lengths are, and the request fails with 502 (nginx/nginx#1637).
  • Slice range starts are ignored near the maximum: With slice 100, a client Range header starting within 100 bytes of NGX_MAX_OFF_T_VALUE overflowed when the slice end was computed as the rounded-down start plus the slice size. Such range starts are now ignored, and the range filter answers 416 because the range is beyond the resource (nginx/nginx#1639).
  • QUIC CRYPTO frames in 1-RTT packets are rejected: A CRYPTO frame arriving after the handshake had completed re-ran the handshake completion path and queued a second key update over secrets the first had already zeroed, so key derivation failed and the connection closed. A client has nothing legitimate to send in a CRYPTO frame at that point—RFC 9001 prohibits TLS KeyUpdate and post-handshake authentication over QUIC, and only servers send NewSessionTicket—so the frame is now rejected with unexpected_message (nginx/nginx#1638).

Why it matters: The first two were undefined behavior, and both aborted binaries built with -fsanitize=signed-integer-overflow. Neither reached out of bounds: without the sanitizer the memcached path streamed until the upstream closed the connection and returned 502, and the slice value simply wrapped until the upstream replied 416. What changes is that you now get those outcomes by design, decided where the length is validated, and a QUIC connection that ends with a stated protocol error rather than a failed key derivation.

Stability and memory-safety fixes

What’s new: Four fixes in buffered proxying, worker shutdown, and JSON rendering:

  • Buffered response buffers are no longer recycled while the downstream still holds them: On a downstream error, ngx_event_pipe_drain_chains() returned every buffer to the free list—including the p->busy chain that the output filters were still using. Over HTTP/1.1 this stayed invisible, because the filter error set c->error on the real connection and the request was finalized immediately. Over HTTP/2 it set the flag on a fake connection instead, so the write handler went on to flush queued DATA frames pointing at freed heap, sending freed memory to the client or crashing the worker on an unmapped page. The busy chain is now left alone and freed with the request pool (nginx/nginx#1664).
  • A worker with no file descriptors left can still shut down: With the descriptor table full, recvmsg() with SCM_RIGHTS cannot read the channel descriptor—BSD and Solaris fail the call, and Linux returns the payload but skips the ancillary data. All of those paths were fatal and closed the worker’s read end of the channel, after which the worker could no longer receive the shutdown signal and never exited. They are now ignored (nginx/nginx#1662).
  • The accept timer is deleted when listen connections close: A worker that had disabled a listen connection because its descriptor table was full set a timer to re-enable it. If NGX_CMD_QUIT arrived during that window, the accept connection was recycled while the timer stayed armed, and its expiry logged accept4() failed (9: Bad file descriptor). The timer is now deleted along with the connection (nginx/nginx#1662).
  • Overflow detection in JSON length calculation is corrected: ngx_json_obj_length() summed a key’s quoted length and its escape expansion before testing for overflow, and compared the running total against the remaining headroom rather than the increment being added to it. Each addition is now checked on its own, in the right direction (nginx/nginx#1658).

Why it matters: The event pipe fix is the primary reason to upgrade. Any configuration that proxies with proxy_buffering on over HTTP/2 could hand a client the contents of freed heap memory, and it took no malicious input to get there—a body filter returning an error while a slow client let DATA frames accumulate was enough, which is why the same drain path stayed invisible over HTTP/1.1. The two worker fixes sit at the other end of the same resource problem: a worker that has exhausted its file descriptors now shuts down cleanly and gets replaced, instead of hanging past a reload or logging a descriptor error after it was told to quit.

Community recognition

We want to give recognition to all the community members who contribute to NGINX. Whether you submitted pull requests, helped refine new architectural features, or reported edge-case bugs, thank you!

Want your name in NGINX 1.31.6?

Pick up an open issue, contribute a feature you developed, or fix a bug you have run into. The contributing guide covers how to get started, from opening a discussion to sending your first pull request.

Getting started

NGINX 1.31.5 is available now as source code and as binary packages for major Linux distributions.

One reminder as you try the Control API: bind it to a UNIX domain socket on a path only privileged users can reach. It is an unauthenticated interface to NGINX internals, and it does not belong on a network port.

We’re proud of what the team built here, and we hope it makes NGINX easier to route with, and easier to operate.

What’s next? Follow-on deep dives

We plan to publish detailed, follow-on technical articles breaking down each of these headline capabilities with full building block examples:

  • Predicate locations and variable-based routing
  • NGINX Control API
  • NGINX routing on HTTP request body

Stay tuned to the NGINX Blog as we share complete configuration building blocks and practical recipes for developers and platform engineering teams!

NGINX Community Forum