Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Nginx HTTP Request Body Internals: Entry, Filters, and Event-Driven Reading

Tech 2

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.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.