Nginx HTTP Request Body Internals: Entry, Filters, and Event-Driven Reading
Nginx defers request-body I/O until phase handlers actually need it (most commonly in the CONTENT phase for proxy_pass and upstream modules). The core path involves two cooperating pieces: a reader that pulls bytes from the socket and a filter chain that moves buffered data to memory or a temporary file when the in-memory window fills.
Request-body read entry
The entry point is ngx_http_read_client_request_body(r, post_handler). It prepares state and, if required, schedules asynchronous reading driven by the event loop. The high-level control flow is equivalent to the following pseudocode:
// equivalent of ngx_http_read_client_request_body
static ngx_int_t http_read_body(ngx_http_request_t *req,
ngx_http_client_body_handler_pt done_cb)
{
ngx_http_request_body_t *body;
ngx_http_core_loc_conf_t *clcf;
size_t unread_in_header;
ssize_t buf_sz;
ngx_int_t rc = NGX_OK;
req->main->count++;
// Subrequests, already-read bodies, or explicitly discarded bodies short-circuit
if (req != req->main || req->request_body || req->discard_body) {
req->request_body_no_buffering = 0;
done_cb(req);
return NGX_OK;
}
// Expect: 100-continue, if needed
if (ngx_http_test_expect(req) != NGX_OK) {
rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
goto out;
}
body = ngx_pcalloc(req->pool, sizeof(*body));
if (body == NULL) { rc = NGX_HTTP_INTERNAL_SERVER_ERROR; goto out; }
body->rest = -1; // means "not initialized yet"
body->post_handler = done_cb;
req->request_body = body;
// No Content-Length and not chunked => nothing to read
if (req->headers_in.content_length_n < 0 && !req->headers_in.chunked) {
req->request_body_no_buffering = 0;
done_cb(req);
return NGX_OK;
}
// Bytes that were already pulled from the socket while parsing headers
unread_in_header = (size_t)(req->header_in->last - req->header_in->pos);
if (unread_in_header) {
ngx_chain_t head = { .buf = req->header_in, .next = NULL };
// Let the body filter account for/capture these bytes
rc = ngx_http_request_body_filter(req, &head);
if (rc != NGX_OK) { goto out; }
req->request_length += unread_in_header
- (req->header_in->last - req->header_in->pos);
// If the rest fits entirely in the header buffer, reuse it as the read window
if (!req->headers_in.chunked && body->rest > 0 &&
body->rest <= (off_t)(req->header_in->end - req->header_in->last))
{
ngx_buf_t *shadow = ngx_calloc_buf(req->pool);
if (shadow == NULL) { rc = NGX_HTTP_INTERNAL_SERVER_ERROR; goto out; }
shadow->temporary = 1;
shadow->start = req->header_in->pos;
shadow->pos = req->header_in->pos;
shadow->last = req->header_in->last;
shadow->end = req->header_in->end;
body->buf = shadow;
req->read_event_handler = ngx_http_read_client_request_body_handler;
req->write_event_handler = ngx_http_request_empty_handler;
rc = ngx_http_do_read_client_request_body(req);
goto out;
}
} else {
// Initialize body->rest, and possibly flush existing chains
if (ngx_http_request_body_filter(req, NULL) != NGX_OK) {
rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
goto out;
}
}
if (body->rest == 0) {
// Nothing more to read
req->request_body_no_buffering = 0;
done_cb(req);
return NGX_OK;
}
if (body->rest < 0) {
ngx_log_error(NGX_LOG_ALERT, req->connection->log, 0,
"negative request body rest");
rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
goto out;
}
clcf = ngx_http_get_module_loc_conf(req, ngx_http_core_module);
// Choose a read window; prefer client_body_buffer_size but shrink for short bodies
buf_sz = clcf->client_body_buffer_size + (clcf->client_body_buffer_size >> 2);
if (!req->headers_in.chunked && body->rest < buf_sz) {
buf_sz = (ssize_t) body->rest;
if (req->request_body_in_single_buf) {
buf_sz += (ssize_t) unread_in_header;
}
} else {
buf_sz = clcf->client_body_buffer_size;
}
body->buf = ngx_create_temp_buf(req->pool, buf_sz);
if (body->buf == NULL) { rc = NGX_HTTP_INTERNAL_SERVER_ERROR; goto out; }
req->read_event_handler = ngx_http_read_client_request_body_handler;
req->write_event_handler = ngx_http_request_empty_handler;
rc = ngx_http_do_read_client_request_body(req);
out:
if (req->request_body_no_buffering && (rc == NGX_OK || rc == NGX_AGAIN)) {
if (rc == NGX_OK) {
req->request_body_no_buffering = 0;
} else {
req->reading_body = 1;
}
req->read_event_handler = ngx_http_block_reading;
done_cb(req);
}
if (rc >= NGX_HTTP_SPECIAL_RESPONSE) {
req->main->count--;
}
return rc;
}
Key behaviors encoded above:
- Skip work for subrequests, already-consumed bodies, or when discarding input.
- Send 100-Continue when required by Expect header.
- Initialize ngx_http_request_body_t and set body->rest from Content-Length (or prepare for chunked).
- If header buffer contains body bytes, pass them through the request-body filter.
- Allocate an in-memory window for body reads.
- Set read_event_handler to continue reading via the event loop, then enter the core reader.
The read path is coordinated through two cooperating components:
- ngx_http_request_body_filter: routes body bytes to memory/file buffers and updates body->rest.
- ngx_http_do_read_client_request_body: pulls bytes off the socket into the current buffer window.
Buffer-to-file path and the request-body filter chain
The dispatcher chooses between chunked decoding and fixed-length handling. Conceptually:
// equivalent of ngx_http_request_body_filter
static ngx_int_t http_body_filter(ngx_http_request_t *req, ngx_chain_t *in)
{
if (req->headers_in.chunked) {
return ngx_http_request_body_chunked_filter(req, in);
}
return http_body_length_filter(req, in);
}
For fixed-length bodies, the filter accounts for how many bytes remain and constructs a chain of buffers to pass to the next filter (which ultimately persists to a temp file if the in-memory window is full):
// equivalent of ngx_http_request_body_length_filter
static ngx_int_t http_body_length_filter(ngx_http_request_t *req, ngx_chain_t *src)
{
ngx_http_request_body_t *body = req->request_body;
ngx_chain_t *link, *tail = NULL, *head = NULL;
if (body->rest == -1) {
body->rest = req->headers_in.content_length_n; // initialize the remaining counter
}
for (link = src; link; link = link->next) {
if (body->rest == 0) { break; }
ngx_chain_t *slot = ngx_chain_get_free_buf(req->pool, &body->free);
if (slot == NULL) { return NGX_HTTP_INTERNAL_SERVER_ERROR; }
ngx_buf_t *b = slot->buf;
ngx_memzero(b, sizeof(*b));
b->temporary = 1;
b->tag = (ngx_buf_tag_t) &http_read_body; // unique tag for recycling
b->start = link->buf->pos;
b->pos = link->buf->pos;
b->last = link->buf->last;
b->end = link->buf->end;
b->flush = req->request_body_no_buffering;
size_t avail = (size_t)(link->buf->last - link->buf->pos);
if ((off_t)avail < body->rest) {
link->buf->pos = link->buf->last;
body->rest -= (off_t)avail;
} else {
link->buf->pos += (size_t) body->rest;
b->last = link->buf->pos;
b->last_buf = 1;
body->rest = 0;
}
slot->next = NULL;
if (head == NULL) { head = slot; } else { tail->next = slot; }
tail = slot;
}
// Push assembled buffers to the top-of-chain filter (saver)
ngx_int_t rc = ngx_http_top_request_body_filter(req, head);
// Recycle buffers; keep those tagged with our tag in the free list
ngx_chain_update_chains(req->pool, &body->free, &body->busy, &head,
(ngx_buf_tag_t) &http_read_body);
return rc;
}
The top of the filter chain typically points to the saver that appends buffers to an in-memory list and spills to a temp file if required by policy or when the active window is full:
// equivalent of ngx_http_request_body_save_filter
static ngx_int_t http_body_save_filter(ngx_http_request_t *req, ngx_chain_t *in)
{
ngx_http_request_body_t *body = req->request_body;
// Append incoming buffers to body->bufs chain
if (ngx_chain_add_copy(req->pool, &body->bufs, in) != NGX_OK) {
return NGX_HTTP_INTERNAL_SERVER_ERROR;
}
// In no-buffering mode, upstream consumes immediately; nothing to spill
if (req->request_body_no_buffering) {
return NGX_OK;
}
if (body->rest > 0) {
// Not done yet. If the current in-memory window is full, spill to file now.
if (body->buf && body->buf->last == body->buf->end) {
if (ngx_http_write_request_body(req) != NGX_OK) {
return NGX_HTTP_INTERNAL_SERVER_ERROR;
}
}
return NGX_OK;
}
// All bytes received. If a temp file is needed or explicitly requested, finalize the spill
if (body->temp_file || req->request_body_in_file_only) {
if (ngx_http_write_request_body(req) != NGX_OK) {
return NGX_HTTP_INTERNAL_SERVER_ERROR;
}
if (body->temp_file->file.offset != 0) {
ngx_chain_t *cl = ngx_chain_get_free_buf(req->pool, &body->free);
if (cl == NULL) { return NGX_HTTP_INTERNAL_SERVER_ERROR; }
ngx_buf_t *b = cl->buf;
ngx_memzero(b, sizeof(*b));
// Represent the on-disk body segment
b->in_file = 1;
b->file = &body->temp_file->file;
b->file_last = body->temp_file->file.offset;
body->bufs = cl;
}
}
return NGX_OK;
}
Important details in the saver:
- Always extend body->bufs by copying chain links; body->rest tracks remaining bytes.
- If buffering and the active buffer is full while more data is expected, call ngx_http_write_request_body to spill.
- After all bytes arrive, ensure final spill if the configuration mandates writing to file; replace body->bufs with a file-backed buffer so downstream consumers can read from disk.
Reading from the socket: the core loop
Bytes are pulled from the client connection by ngx_http_do_read_client_request_body. The loop packs as many bytes as possible into the active buffer before yielding control back to the event loop when the socket would block:
// equivalent of ngx_http_do_read_client_request_body
static ngx_int_t http_do_read_body(ngx_http_request_t *req)
{
ngx_http_request_body_t *body = req->request_body;
ngx_connection_t *conn = req->connection;
for (;;) {
for (;;) {
// If our current in-memory window is full, flush it through the filter
if (body->buf->last == body->buf->end) {
ngx_int_t rc;
ngx_chain_t single = { .buf = body->buf, .next = NULL };
if (body->buf->pos != body->buf->last) {
rc = ngx_http_request_body_filter(req, &single);
} else {
rc = ngx_http_request_body_filter(req, NULL);
}
if (rc != NGX_OK) { return rc; }
if (body->busy != NULL) {
if (req->request_body_no_buffering) {
if (conn->read->timer_set) { ngx_del_timer(conn->read); }
if (ngx_handle_read_event(conn->read, 0) != NGX_OK) {
return NGX_HTTP_INTERNAL_SERVER_ERROR;
}
return NGX_AGAIN; // let upstream consume
}
return NGX_HTTP_INTERNAL_SERVER_ERROR; // cannot progress
}
// Reset window to start
body->buf->pos = body->buf->start;
body->buf->last = body->buf->start;
}
// Compute how much we can read this iteration
size_t room = (size_t)(body->buf->end - body->buf->last);
off_t remain = body->rest - (body->buf->last - body->buf->pos);
if ((off_t)room > remain) { room = (size_t) remain; }
// Try recv() into the window
ssize_t n = conn->recv(conn, body->buf->last, room);
if (n == NGX_AGAIN) { break; } // would block
if (n == 0) {
ngx_log_error(NGX_LOG_INFO, conn->log, 0,
"client prematurely closed connection");
}
if (n == 0 || n == NGX_ERROR) {
conn->error = 1;
return NGX_HTTP_BAD_REQUEST;
}
body->buf->last += n;
req->request_length += n;
// If we just satisfied the remaining bytes in this window, push it downstream
if ((off_t)n == remain) {
ngx_chain_t one = { .buf = body->buf, .next = NULL };
ngx_int_t rc = ngx_http_request_body_filter(req, &one);
if (rc != NGX_OK) { return rc; }
}
if (body->rest == 0) { break; } // all bytes accounted for
if (body->buf->last < body->buf->end) { break; } // still room; let outer loop manage readiness
}
if (body->rest == 0) { break; }
// No more readable data right now; arm timer and wait for next event
if (!conn->read->ready) {
if (req->request_body_no_buffering && body->buf->pos != body->buf->last) {
ngx_chain_t out = { .buf = body->buf, .next = NULL };
ngx_int_t rc = ngx_http_request_body_filter(req, &out);
if (rc != NGX_OK) { return rc; }
}
ngx_http_core_loc_conf_t *clcf =
ngx_http_get_module_loc_conf(req, ngx_http_core_module);
ngx_add_timer(conn->read, clcf->client_body_timeout);
if (ngx_handle_read_event(conn->read, 0) != NGX_OK) {
return NGX_HTTP_INTERNAL_SERVER_ERROR;
}
return NGX_AGAIN;
}
}
// Reading complete; stop the timer and hand control to the post handler
if (conn->read->timer_set) { ngx_del_timer(conn->read); }
if (!req->request_body_no_buffering) {
req->read_event_handler = ngx_http_block_reading;
body->post_handler(req);
}
return NGX_OK;
}
Operational notes for the loop:
- The inner loop aggressively consumes as many bytes as available into the in-memory window to minimize event churn.
- When recv() would block (NGX_AGAIN), the function arms the read timer and yields. The event loop will invoke the read_event_handler again on readiness.
- body->rest reflects the remaining payload, either decoded from Content-Length or from the chunked parser.
- In unbuffered mode (request_body_no_buffering), the reader intermittently returns NGX_AGAIN to let downstream consumers (e.g., upstream modules) process chunks as they arrive.
Where phase handlers fit
After headers are parsed, control proceeds to the HTTP phase engine. If a content handler (e.g., proxy_pass) needs the body, it calls ngx_http_read_client_request_body and installs a post_handler callback. While the connection’s read events fire, Nginx invokes ngx_http_read_client_request_body_handler, which calls ngx_http_do_read_client_request_body. Each readable event contineus the cycle until body->rest reaches zero, at which point the post_handler is invoked with the body available in memory and/or on disk.