namalzi

← All posts

Digging into Apache httpd's Source 11: Filters and Bucket Brigades — How a Response Flows Out

Apache httpd · Published

What shape a response takes inside httpd on its way from the handler to the socket, followed for someone opening the source for the first time. APR-util's buckets and brigades (a ring), PIPE and FILE buckets that change shape when read, filters lining up in six tiers and ap_pass_brigade, chunked encoding and sendfile, and what the core output filter does when the socket is full — all checked with the log of a filter module written for this post.

What this post covers

This is the eleventh post in the "Digging into Apache httpd's Source" series. Part 10 followed one request from its URL to a file and a handler. Once the handler is called, it starts producing the response. This time we look at what shape that response takes, and who it passes through, on its way to the socket.

There are four questions to answer.

Item Details
Target Apache httpd
Version 2.5.1-dev (trunk)
Commit e4af30fe6d (2026-10-24)
APR-util 1.6.3 (tag 1.6.3, commit 28327eeca2) — buckets and brigades
APR 1.7.2 (tag 1.7.2, commit e905526629) — the ring macros
Main files APR-util include/apr_buckets.h, buckets/apr_buckets_*.c / httpd include/util_filter.h, server/util_filter.c, server/core_filters.c, modules/http/http_filters.c, modules/http/chunk_filter.c

All line numbers refer to this commit. How to get the same source, and the Docker setup used to run the examples, are in Part 1. Filters and buckets are spread widely across the code, so this post keeps to the main path that a newcomer can follow. What was left out is listed by name only in "What this post leaves out" at the end.

Words to know first

A bucket is a small struct that points at one piece of the response (or request) data. It records where the data is: "these 15 bytes in memory", "1000 bytes of this file from offset 0", "whatever will come out of this pipe". The key point is that it doesn't necessarily hold the data itself.

A brigade (bucket brigade) is a list of buckets chained together. The name comes from a bucket brigade at a fire, where people stand in a line and pass buckets of water from hand to hand. What httpd's filters pass from hand to hand is this brigade.

A metadata bucket is a marker bucket with no data. It carries signals such as "the response is over" (EOS) or "send out what you have so far" (FLUSH), standing in the same line as the data.

A filter is a function that receives a brigade, does something with it and passes it on to the next filter. Compression (mod_deflate), SSI (mod_include), chunked encoding and TLS encryption (mod_ssl) are all filters. Output filters, through which the response leaves, and input filters, through which the request comes in, line up separately.

A function pointer table is the struct holding function addresses that we saw in Part 2. It is C's way of doing what a Java interface does — "an operation with the same name that works differently for each kind" — and bucket types are built exactly this way. Pools (apr_pool_t) were in Part 4 and hooks in Part 6.

1. Buckets — a note saying "where the data is"

Buckets and brigades live in APR-util, not in httpd; httpd just uses them. Let's start with the bucket struct. (/* ... */ marks places shortened for this post; the rest is the source as is.)

struct apr_bucket {
    /** Links to the rest of the brigade */
    APR_RING_ENTRY(apr_bucket) link;
    /** The type of bucket.  */
    const apr_bucket_type_t *type;
    /* ... */
    apr_size_t length;
    /* ... */
    apr_off_t start;
    /** type-dependent data hangs off this pointer */
    void *data;
    /* ... */
    void (*free)(void *e);
    /** The freelist from which this bucket was allocated */
    apr_bucket_alloc_t *list;
};

APR-util include/apr_buckets.h lines 224–255

The kind, apr_bucket_type_t, is a name, whether it is data or metadata, and five function pointers.

struct apr_bucket_type_t {
    /* ... */
    const char *name;
    /* ... */
    int num_func;
    /* ... */
    enum {
        /** This bucket type represents actual data to send to the client. */
        APR_BUCKET_DATA = 0,
        /** This bucket type represents metadata. */
        APR_BUCKET_METADATA = 1
    } is_metadata;
    /* ... */
    void (*destroy)(void *data);
    /* ... */
    apr_status_t (*read)(apr_bucket *b, const char **str, apr_size_t *len, 
                         apr_read_type_e block);
    /* ... */
    apr_status_t (*setaside)(apr_bucket *e, apr_pool_t *pool);
    /* ... */
    apr_status_t (*split)(apr_bucket *e, apr_size_t point);
    /* ... */
    apr_status_t (*copy)(apr_bucket *e, apr_bucket **c);

};

APR-util include/apr_buckets.h lines 131–213

Function What it does
read Reads the data and returns a pointer and a length. If needed, it turns the bucket itself into a different kind (section 3)
setaside Moves the data into a longer-lived pool so it survives after the current function returns (section 5)
split Cuts one bucket into two (copying only the struct, not the data)
copy Makes another bucket pointing at the same data
destroy Cleans up the contents when the bucket is deleted

There is one such table per kind, as a global constant. Here is the table for the EOS bucket, for example.

APU_DECLARE_DATA const apr_bucket_type_t apr_bucket_type_eos = {
    "EOS", 5, APR_BUCKET_METADATA,
    apr_bucket_destroy_noop,
    eos_bucket_read,
    apr_bucket_setaside_noop,
    apr_bucket_split_notimpl,
    apr_bucket_simple_copy
};

APR-util buckets/apr_buckets_eos.c lines 47–54

And "read the bucket" is a single macro: #define apr_bucket_read(e,str,len,block) (e)->type->read(e, str, len, block) (line 1089). The caller doesn't need to know what kind the bucket is; each kind reads in its own way. Even the macros that ask what kind a bucket is just compare the address of this table — APR_BUCKET_IS_EOS(e) is ((e)->type == &apr_bucket_type_eos) (line 461).

The type field of the apr_bucket struct points at an apr_bucket_type_t table, one shared per kind, which holds five function pointers: destroy, read, setaside, split and copy. The data field points at kind-specific contents (a file handle for FILE, memory for HEAP). apr_bucket_read expands to a call to e->type->read. Yellow is what sits in memory; blue is the function pointer table. The "struct + function pointers" pattern from Part 2, used exactly as is.

APR-util ships ten kinds (lines 1188–1235).

Kind Data / meta What it points at
HEAP data Memory from malloc. Freed together with the bucket
POOL data Memory from a pool. Moved to the heap if the pool goes away first
TRANSIENT data Memory valid only briefly (e.g. a function's local array). Must be copied with setaside to keep it
IMMORTAL data Memory that lives for the whole program (string constants, etc.). Nothing is done with it
FILE data A range of an open file
MMAP data A memory-mapped (mmap) range of a file
PIPE data A pipe. Length unknown
SOCKET data A socket. Length unknown
EOS meta End of the response (stream)
FLUSH meta Send everything so far onward now

httpd adds kinds of its own — all it takes is another table. Under server/ there are eor_bucket.c (EOR — end of one request), eoc_bucket.c (EOC — end of the connection), error_bucket.c (ERROR — an HTTP error status) and headers_bucket.c (REQUEST, RESPONSE, HEADERS), all metadata buckets. The EOR bucket, for instance, destroys the request pool when it is deleted.

static void eor_bucket_destroy(void *data)
{
    ap_bucket_eor *h = data;

    if (apr_bucket_shared_destroy(h)) {
        request_rec *r = h->r;
        if (r) {
            /* eor_bucket_cleanup will be called when the pool gets destroyed */
            apr_pool_destroy(r->pool);
        }
        apr_bucket_free(h);
    }
}

server/eor_bucket.c lines 100–112

That pool's cleanup function, eor_bucket_cleanup(), calls ap_run_log_transaction(r), which writes the access log (lines 27–46). When the handler is done, ap_process_request_after_handler() creates this EOR bucket and sends it straight to the connection filters (modules/http/http_request.c lines 336–363). So a request's memory is freed not "when the handler finishes" but "after the last byte of the response has been sent to the socket". If the request pool disappeared while the response was still sitting in a buffer, things would break. The access log comes back in Part 16.

Easy to get wrong: headers_bucket.c exists only in trunk

The REQUEST, RESPONSE and HEADERS buckets live in a file the 2.4.x branch doesn't have. In trunk, even the response headers travel down the filters as a metadata bucket called RESPONSE and only become text at the filter that writes HTTP/1.x (section 6). In 2.4.x, the header filter writes the header text directly.

2. Brigades — a ring whose end joins its start

The brigade struct is short.

struct apr_bucket_brigade {
    /* ... */
    apr_pool_t *p;
    /* ... */
    APR_RING_HEAD(apr_bucket_list, apr_bucket) list;
    /** The freelist from which this bucket was allocated */
    apr_bucket_alloc_t *bucket_alloc;
};

APR-util include/apr_buckets.h lines 258–276

APR_RING_HEAD, and APR_RING_ENTRY on the bucket side, are APR's ring macros. A ring is "a doubly-linked list that can be manipulated without knowing where its head is" (APR include/apr_ring.h lines 43–44). Expand the macro and it is just a struct with two pointers.

#define APR_RING_ENTRY(elem)						\
    struct {								\
	struct elem * volatile next;					\
	struct elem * volatile prev;					\
    }

APR include/apr_ring.h lines 70–74

#define is a macro that substitutes text before compilation, and the \ at the end of a line means "the macro continues on the next line" (Part 2). What is unusual about a ring is that the head (the brigade) is treated as one slot of the ring too. The last bucket's next is not NULL but points to the head, and that head is called the sentinel. That is why code that walks a brigade always looks like this.

for (e = APR_BRIGADE_FIRST(bb);
     e != APR_BRIGADE_SENTINEL(bb);
     e = APR_BUCKET_NEXT(e)) {
    /* ... */
}

This is taken from this post's example module (describe()) with the body shortened; almost every filter in httpd has a loop of the same shape.

A ring: from the brigade head (the sentinel), next leads to HEAP(228) response headers, FILE(26) index.html and EOS(0) end marker, and the next of EOS leads back to the head The real brigade seen just above the socket when one static file is sent (output of Example 1). Grey is the head, which holds no data.

The beauty of a ring is that joining and cutting only takes a few pointer changes. APR_BRIGADE_CONCAT(a, b), which joins two brigades (lines 380–383), APR_BUCKET_INSERT_BEFORE, which inserts a bucket, and apr_brigade_split_ex(), which cuts a brigade in two at some bucket, don't move a single byte of data. Filters often insert new buckets between the ones they received (the length line of chunked), remove buckets (the original before compression), or swap buckets for others (the compressed result), and none of that involves copying.

3. Why it's built this way — don't copy, read late

Imagine passing a single char * buffer from filter to filter instead of buckets, and the reasons for this design become clear.

Sending a static file doesn't require reading the file. default_handler() (the handler that sends static files, Part 12) doesn't open the file and read its contents; it sends one FILE bucket and one EOS.

            e = apr_brigade_insert_file(bb, fd, 0, r->finfo.size, r->pool);

server/core.c line 5264

If this bucket reaches the core output filter at the bottom untouched, the core filter can use Linux's sendfile() system call to send it from the file to the socket entirely inside the kernel (section 5). There's no copying of the data up into user memory and back down, which is why this is often called "zero-copy".

Data you only know by reading changes shape when it is read. Look at the PIPE bucket's read: it turns itself into a HEAP bucket holding what it just read from the pipe, and inserts a new PIPE bucket for the rest of the pipe right after it.

    if (*len > 0) {
        apr_bucket_heap *h;
        /* Change the current bucket to refer to what we read */
        a = apr_bucket_heap_make(a, buf, *len, apr_bucket_free);
        h = a->data;
        h->alloc_len = APR_BUCKET_BUFF_SIZE; /* note the real buffer size */
        *str = buf;
        APR_BUCKET_INSERT_AFTER(a, apr_bucket_pipe_create(p, a->list));
    }
    else {
        apr_bucket_free(buf);
        a = apr_bucket_immortal_make(a, "", 0);
        *str = a->data;
        if (rv == APR_EOF) {
            apr_file_close(p);
        }
    }

APR-util buckets/apr_buckets_pipe.c lines 58–74

When everything has been read and nothing more will come, it becomes a zero-length IMMORTAL bucket and closes the pipe. SOCKET buckets work the same way. FILE buckets change when read too — into an MMAP bucket if mmap can be used, otherwise into a HEAP bucket for what was read plus a FILE bucket for the rest (buckets/apr_buckets_file.c lines 75–152). The source comments call such buckets that change kind when read "morphing buckets" (server/core_filters.c lines 552–553).

When someone reads a PIPE(?) EOS brigade it becomes HEAP(17) PIPE(?) EOS, and when it is read again and the pipe has ended it becomes HEAP(17) IMMORTAL(0) EOS The change that actually happened in Example 4 (section 6). A CGI program's output flows through a pipe bucket in the same way (mod_cgi.c line 754).

This means a response of unknown size, such as a CGI program's output, can be left unread until someone actually needs it. There's no need to gather it all in memory first.

Example 1 — printing the buckets a filter receives

Talk only goes so far, so let's check it directly. I wrote a small module that registers two output filters. One is plugged in at the top of the content side (AP_FTYPE_RESOURCE) and one right above the socket (AP_FTYPE_CONNECTION); both print the kinds and lengths of the buckets in the brigade they receive on one line and pass it on unchanged. Filter types are explained in section 4.

/* Example written for this post (not httpd's source) */
/* ... */

/* The same function is registered under two names. f->ctx counts the calls */
static apr_status_t log_filter(ap_filter_t *f, apr_bucket_brigade *bb)
{
    int *n = f->ctx;

    if (n == NULL) {
        n = f->ctx = apr_pcalloc(f->c->pool, sizeof(*n));
    }
    ++*n;
    if (*n == 1 && f->r) {   /* first call of a request filter: print the chain below */
        const char *chain = f->frec->name;
        ap_filter_t *g;
        for (g = f->next; g; g = g->next) {
            chain = apr_pstrcat(f->c->pool, chain, " -> ", g->frec->name, NULL);
        }
        ap_log_cerror(APLOG_MARK, APLOG_NOTICE, 0, f->c, "chain: %s", chain);
    }
    ap_log_cerror(APLOG_MARK, APLOG_NOTICE, 0, f->c, "%-13s #%d:%s",
                  f->frec->name, *n, describe(bb, f->c->pool));
    return ap_pass_brigade(f->next, bb);       /* hand it on untouched */
}

/* ... */

/* Per request: plug BUCKETLOG_TOP in at the top of the content side */
static void insert_filters(request_rec *r)
{
    ap_add_output_filter("BUCKETLOG_TOP", NULL, r, r->connection);
}

/* Per connection: plug BUCKETLOG_NET in right above the socket (before CORE) */
static int pre_conn(conn_rec *c, void *csd)
{
    ap_add_output_filter("BUCKETLOG_NET", NULL, NULL, c);
    return OK;
}

static void register_hooks(apr_pool_t *p)
{
    ap_register_output_filter("BUCKETLOG_TOP", log_filter, NULL,
                              AP_FTYPE_RESOURCE);
    ap_register_output_filter("BUCKETLOG_NET", log_filter, NULL,
                              AP_FTYPE_CONNECTION);
    /* ... */
    ap_hook_insert_filter(insert_filters, NULL, NULL, APR_HOOK_FIRST);
    ap_hook_pre_connection(pre_conn, NULL, NULL, APR_HOOK_MIDDLE);
    /* ... */
}

describe() uses the loop from section 2 to join e->type->name and e->length (? when the length is unknown). This example was run in a Docker container (Ubuntu 24.04) with httpd 2.5.1-dev built as is from the reference commit (e4af30fe6d). ErrorLogFormat "%M" leaves only the message in the error log, and curl fetched one static file (26 bytes).

$ curl http://127.0.0.1:8080/index.html
Hello from a static file.
--- error_log
chain: bucketlog_top -> byterange -> content_length -> http_header -> http_outerror -> http1_response_out -> bucketlog_net -> core
bucketlog_top #1: FILE(26) EOS(0)
bucketlog_net #1: HEAP(228) FILE(26) EOS(0)
bucketlog_net #2: EOR(0)
bucketlog_net #3: FLUSH(0) EOC(0)

There is a lot to read here.

  1. What arrived at the top was just two buckets, FILE and EOS. Nobody has read the file's contents yet.
  2. Just above the socket, a HEAP(228) has been added in front. That's the 228 bytes of status line and headers. The header filters merely inserted one bucket before FILE; FILE came down unchanged.
  3. The next brigade is a lone EOR. This is the "end of request" marker from section 1; when it is deleted, the request pool is cleaned up. This bucket starts at the connection filters, so bucketlog_top never sees it.
  4. Last come FLUSH and EOC. These were sent when curl closed the connection and httpd tidied it up (ap_shutdown_conn(), server/connection.c lines 104–125).
  5. The filter names are printed in lowercase. Section 4 explains why.

Easy to get wrong: a filter is called many times per response

bucketlog_net was called three times for one request. A filter function isn't "a function that receives the whole response at once"; it is called every time a brigade arrives. So anything a filter must remember across calls goes in f->ctx (where the example counts calls). The comment in util_filter.h says the same — the callback finds its context in filter->ctx (include/util_filter.h lines 103–105).

4. Filters — lining up in six tiers

A filter is split across two structs: the registration (ap_filter_rec_t — name, function, type; one for the whole server) and one slot plugged into a chain (ap_filter_t — created anew per request or connection).

struct ap_filter_t {
    /* ... */
    ap_filter_rec_t *frec;

    /** A place to store any data associated with the current filter */
    void *ctx;

    /** The next filter in the chain */
    ap_filter_t *next;

    /* ... */
    request_rec *r;

    /* ... */
    conn_rec *c;

    /** Filter private/opaque data */
    struct ap_filter_private *priv;
};

include/util_filter.h lines 278–303

It's a singly-linked list joined by a single next. An output filter function has the shape apr_status_t (*ap_out_filter_func)(ap_filter_t *f, apr_bucket_brigade *b) (lines 134–135). The last field, priv, is new in trunk; in 2.4.x ap_filter_t ends at c. Section 5's "holding on to what couldn't be sent" uses it.

A filter's type (ftype) is a number, and the number decides the order of the line.

typedef enum {
    /** These filters are used to alter the content that is passed through
     *  them. Examples are SSI or PHP. */
    AP_FTYPE_RESOURCE     = 10,
    /* ... */
    AP_FTYPE_CONTENT_SET  = 20,
    /** These filters are used to handle the protocol between server and
     *  client.  Examples are HTTP and POP. */
    AP_FTYPE_PROTOCOL     = 30,
    /** These filters implement transport encodings (e.g., chunking). */
    AP_FTYPE_TRANSCODE    = 40,
    /* ... */
    AP_FTYPE_CONNECTION  = 50,
    /** These filters don't alter the content.  They are responsible for
     *  sending/receiving data to/from the client. */
    AP_FTYPE_NETWORK     = 60
} ap_filter_type;

include/util_filter.h lines 158–184

Type What it does Examples in this commit
RESOURCE (10) Changes the content itself INCLUDES (SSI)
CONTENT_SET (20) Changes the content as a whole but keeps the Content-Type DEFLATE
PROTOCOL (30) Protocol handling, such as HTTP HTTP_HEADER, CONTENT_LENGTH, BYTERANGE
TRANSCODE (40) Transfer encodings CHUNK, HTTP1_RESPONSE_OUT
CONNECTION (50) Work tied to the connection SSL/TLS Filter (mod_ssl's TLS filter, AP_FTYPE_CONNECTION + 5 = 55, Part 15)
NETWORK (60) Actually sends and receives CORE, CORE_IN

The types of DEFLATE and INCLUDES were checked at each module's registration line (mod_deflate.c lines 1927–1928, mod_include.c lines 4247–4248). The filter registrations of the core and the HTTP module are gathered in server/core.c lines 6199–6213 and modules/http/http_core.c lines 369–392. Numbers between the six values can be used too: mod_ssl registers with AP_FTYPE_CONNECTION + 5, i.e. 55 (modules/ssl/ssl_engine_io.c lines 2307–2309), and the core's OLD_WRITE registers with AP_FTYPE_RESOURCE - 10, i.e. 0 (section 6).

Registering — giving it a name

A module calls ap_register_output_filter(name, function, init function, type) from the function that registers its hooks (register_hooks, Part 6). The example's register_hooks has this shape. The registration function lowercases the name and stores it in a character tree (trie).

    normalized_name = apr_pstrdup(FILTER_POOL, name);
    ap_str_tolower(normalized_name);

server/util_filter.c lines 250–251

That's why the example log showed names like bucketlog_top in lowercase. To make lookups case-insensitive, it also links an uppercase branch for each letter (lines 256–258). So writing SetOutputFilter deflate or DEFLATE in the config finds the same filter.

Plugging in — three routes

Registering alone does nothing. A filter is only called once it is plugged into a request's (or connection's) chain. There's one function for that, ap_add_output_filter(name, ctx, r, c), called from several places.

  1. The config file — SetOutputFilter UPPER is stored as a string in the core's per-directory config, and the core's insert_filter hook splits those names on ; and plugs them in one by one (server/core.c lines 5417–5435). AddOutputFilterByType is a directive of mod_filter (modules/filters/mod_filter.c line 750).
  2. The insert_filter hook — ap_invoke_handler() runs this hook just before calling the handler (server/config.c line 405). The example's BUCKETLOG_TOP takes this route. The order of the request-processing steps is in Part 10.
  3. Directly from code — the HTTP module plugs in BYTERANGE, CONTENT_LENGTH, HTTP_HEADER and HTTP_OUTERROR when a request is created (modules/http/http_core.c lines 257–271), and the core plugs in CORE_IN and CORE when a connection is created (server/core.c lines 5690–5691). The example's BUCKETLOG_NET was plugged in from the pre_connection hook.

Whichever route is taken, the position is decided by the type number. This is the comparison macro inside the plug-in function.

#define INSERT_BEFORE(f, before_this) ((before_this) == NULL \
| (before_this)->frec->ftype > (f)->frec->ftype \
| (before_this)->r != (f)->r)

server/util_filter.c lines 42–44

A new filter goes in front of any filter with a larger number. With an equal number it goes after the ones already plugged in. The comment in util_filter.h says the same: "within a particular grouping, the ordering is equivalent to the order of calls to ap_add_*_filter()" (lines 150–157).

And the chain has three starting points. add_any_filter_handle() chooses which list to put a filter in based on its type (server/util_filter.c lines 431–452). Below PROTOCOL it's r->output_filters, below CONNECTION r->proto_output_filters, and the rest c->output_filters. The three are not separate lists but different points of one chain: the end of the request filters links to the start of the connection filters. So request filters disappear with the request, while connection filters stay in place for the next request as long as the connection is kept alive.

Going down from the handler through RESOURCE(10), CONTENT_SET(20), PROTOCOL(30), TRANSCODE(40), CONNECTION(50) and NETWORK(60) to the client. r->output_filters points at the RESOURCE point, r->proto_output_filters at the PROTOCOL point and c->output_filters at the CONNECTION point. The names in the blue boxes are the filters actually printed by the example plus typical filters of this commit. The yellow boxes show which point of the chain each pointer refers to.

The chain: line of Example 1 is exactly this order — bucketlog_top (10) → byterange, content_length, http_header, http_outerror (30) → http1_response_out (40) → bucketlog_net (50) → core (60).

Handing over — ap_pass_brigade()

The function a filter uses to hand over to the next one is surprisingly short.

AP_DECLARE(apr_status_t) ap_pass_brigade(ap_filter_t *next,
                                         apr_bucket_brigade *bb)
{
    if (next) {
        apr_bucket *e = APR_BRIGADE_LAST(bb);

        if (e != APR_BRIGADE_SENTINEL(bb) && APR_BUCKET_IS_EOS(e) && next->r) {
            /* ... */
            next->r->eos_sent = 1;

            /* ... */
        }
        return next->frec->filter_func.out_func(next, bb);
    }
    return AP_NOBODY_WROTE;
}

server/util_filter.c lines 756–786

If the last bucket is EOS, it marks "this request has sent its end", then calls the next filter's function pointer. That's all. When filter A calls ap_pass_brigade(f->next, bb), filter B is called inside it, B calls C, and so on — the calls stack up on the way down. Only when core at the bottom has written to the socket and returned do the filters above return in turn. It's a structure that "pushes" data down.

Example 2 — a filter that changes content, and a bucket that changes shape

I added an UPPER filter to the same module, which reads data buckets and swaps them for HEAP buckets converted to uppercase, and plugged it in with SetOutputFilter UPPER in the config.

        before = e->type->name;
        rv = apr_bucket_read(e, &data, &len, APR_BLOCK_READ);
        if (rv != APR_SUCCESS) {
            return rv;
        }
        /* after reading, the same bucket may have become a different kind */
        ap_log_cerror(APLOG_MARK, APLOG_NOTICE, 0, f->c,
                      "upper: read %s -> now %s(%" APR_SIZE_T_FMT ")",
                      before, e->type->name, e->length);
        buf = apr_bucket_alloc(len, f->c->bucket_alloc);
        for (i = 0; i < len; i++) {
            buf[i] = apr_toupper(data[i]);
        }
        upper = apr_bucket_heap_create(buf, len, apr_bucket_free,
                                       f->c->bucket_alloc);
        APR_BUCKET_INSERT_BEFORE(e, upper);
        apr_bucket_delete(e);                       /* delete the original bucket */
        e = upper;
$ curl http://127.0.0.1:8080/upper/index.html
HELLO FROM A STATIC FILE.
--- error_log
chain: bucketlog_top -> upper -> byterange -> content_length -> http_header -> http_outerror -> http1_response_out -> bucketlog_net -> core
bucketlog_top #1: FILE(26) EOS(0)
upper: read FILE -> now MMAP(26)
bucketlog_net #1: HEAP(228) HEAP(26) EOS(0)
bucketlog_net #2: EOR(0)
bucketlog_net #3: FLUSH(0) EOC(0)

Easy to get wrong: you can't hold on to received buckets until the next call

Processing everything on the spot, as UPPER does, is fine. But a filter that wants to "collect a bit and process it all at once" is tempted to stash received buckets in f->ctx. A TRANSIENT bucket, however, points at memory that may vanish once the function that handed it over returns. So util_filter.h says that to keep buckets until the next call you must "set them aside" with apr_bucket_setaside(), typically via ap_save_brigade() (include/util_filter.h lines 117–122). A TRANSIENT bucket's setaside copies the data to the heap.

5. At the bottom — the core output filter writes to the socket

The CORE filter at the end of the chain is ap_core_output_filter() (server/core_filters.c lines 359–443). Its main path looks like this.

    /* Prepend buckets set aside, if any. */
    ap_filter_reinstate_brigade(f, bb, NULL);
    if (APR_BRIGADE_EMPTY(bb)) {
        return APR_SUCCESS;
    }

    /* Non-blocking writes on the socket in any case. */
    apr_socket_timeout_get(sock, &sock_timeout);
    apr_socket_timeout_set(sock, 0);

    do {
        rv = send_brigade_nonblocking(sock, bb, ctx, c);
        /* ... */
    } while (rv == APR_SUCCESS && !APR_BRIGADE_EMPTY(bb));
    /* ... */
    return ap_filter_setaside_brigade(f, bb);
}

server/core_filters.c lines 389–443

  1. Leftovers from last time first. If buckets were set aside in an earlier call, they are put in front of this brigade.
  2. Make the socket non-blocking. With the timeout at 0, if the socket can't take more, the call returns EAGAIN ("not now") instead of waiting.
  3. Send as much as possible — send_brigade_nonblocking().
  4. Set aside the rest and return — ap_filter_setaside_brigade().

send_brigade_nonblocking() (lines 514–647) walks the buckets from the front and splits them two ways.

#if APR_HAS_SENDFILE
        if (can_sendfile_bucket(bucket)) {
            if (nvec > 0) {
                sock_nopush(s, 1);
                rv = writev_nonblocking(s, bb, ctx, nbytes, nvec, c);
                if (rv != APR_SUCCESS) {
                    goto cleanup;
                }
                nbytes = 0;
                nvec = 0;
            }
            rv = sendfile_nonblocking(s, bucket, ctx, c);
            if (rv != APR_SUCCESS) {
                goto cleanup;
            }
            continue;
        }
#endif /* APR_HAS_SENDFILE */

        if (bucket->length) {
            /* Non-blocking read first, in case this is a morphing
             * bucket type. */
            rv = apr_bucket_read(bucket, &data, &length, APR_NONBLOCK_READ);

server/core_filters.c lines 532–554

can_sendfile_bucket() has three conditions: it is a FILE bucket, its length is at least AP_MIN_SENDFILE_BYTES (256), and the file was opened with EnableSendfile On (lines 467–480, line 57). EnableSendfile is off by default: without the directive, the AP_SENDFILE_ENABLED() macro yields 0 (include/http_core.h lines 689–691).

Example 3 — is sendfile actually called?

I fetched a 1000-byte file through a path without EnableSendfile (/big.txt) and one with it turned on (/sf/big.txt), and used strace to pick out only the writev and sendfile system calls httpd made. httpd was started with -X (run as a single process). The number at the start of each line is a thread ID; it and the address values differ on every run.

--- http://127.0.0.1:8080/big.txt
274   writev(8, 0x7fac5c0b6a30, 2)      = 1232
--- http://127.0.0.1:8080/sf/big.txt
276   writev(8, 0x7fac5c0b6a30, 1)      = 232
276   sendfile(8, 9, 0x7fac57ffe458, 1000) = 1000

In both cases the filter log was the same: HEAP(232) FILE(1000) EOS(0). The only difference is what happened at the bottom. With it off, the 232 bytes of headers and the 1000 bytes of the file went out in one two-piece writev (1232 bytes); with it on, only the headers went through writev and the file went through sendfile. This choice is possible at the bottom because the bucket carried "where the data is" rather than "the data itself".

When the socket is full — setting aside and flow control

When the client is slow, the socket's send buffer fills up and writev returns EAGAIN. The core filter doesn't wait right away; it asks ap_filter_reinstate_brigade() "how far into what's left must go out right now?". That function's comment lists three rules (server/util_filter.c lines 1111–1135).

  1. If there's a FLUSH bucket, send everything up to that FLUSH, blocking if necessary.
  2. If the bytes in memory buckets (not files) exceed FlushMaxThreshold, send that much. This is flow control: it stops memory from growing without limit when a handler produces data much faster than the client can take it.
  3. If the EORs (finished requests) piled up by pipelining exceed FlushMaxPipelined, send. The point is to keep finished requests from holding on to too many file handles.

The defaults are 65535 bytes and 4 (server/core.c lines 91–92). File buckets and buckets of unknown length are left out of rule 2's count, because "they use no memory until read" (same comment). If none of the three applies, the remaining buckets are set aside with ap_filter_setaside_brigade() in a connection-side pool (deferred_pool) and the filter returns (lines 946–1047).

The flow of the core output filter. Set-aside buckets are put in front and sent non-blocking. If all is sent, success; on EAGAIN it checks whether it must send now (FLUSH, FlushMaxThreshold, FlushMaxPipelined). If so it waits with apr_poll and sends again; if not it sets the rest aside and returns, and the event MPM resumes sending when the socket becomes writable. The yellow boxes are "set aside" and "reinstate". Grey is the part handled by the event MPM from Part 8.

When something is set aside, ap_filter_should_yield() becomes true (lines 1218–1268). After a request is processed, the event MPM checks this; if output is still pending, it puts the connection on the write-waiting queue and lets the worker thread go (server/mpm/event/event.c lines 1260–1261). This keeps a single slow client from tying up a thread.

Easy to get wrong: this "set aside" API belongs to trunk

ap_filter_setaside_brigade(), ap_filter_reinstate_brigade() and ap_filter_should_yield() aren't in 2.4.x's server/util_filter.c. In 2.4.x, the core filter does the same job on its own with static functions inside core_filters.c (setaside_remaining_output(), should_send_brigade()). Trunk lifted that into a general API so that other connection filters such as mod_ssl can set aside and yield in the same way (ssl_engine_io.c calls these functions). The FlushMaxThreshold and FlushMaxPipelined directives exist in 2.4.x too; the AsyncFilter directive, which sets the range of filters allowed to yield, exists only in trunk.

6. HTTP filters — headers and chunked encoding

Who made the HEAP(228) in front of FILE in Example 1? In trunk, two filters share the job.

  1. HTTP_HEADER (PROTOCOL) — when it sees the first data bucket or EOS, it inserts a RESPONSE metadata bucket in front of it, carrying the status code and the header table (modules/http/http_filters.c lines 1284–1285).
  2. HTTP1_RESPONSE_OUT (TRANSCODE) — when it meets the RESPONSE bucket, it writes HTTP/1.1 200 OK and the header lines as text in its place and deletes the RESPONSE bucket (lines 2000–2003). This filter is only plugged in for HTTP/1.x connections (modules/http/http_core.c lines 274–285).

The reason for sending the headers down as a bucket instead of writing them as text right away seems to be that anything writing the same response differently, such as HTTP/2, then only needs to read the one RESPONSE bucket. That's a guess from the structure, though, not a reason confirmed by a source comment.

Chunked encoding is HTTP/1.1's way of sending a response whose length can't be put in the headers in advance. The body is sent in pieces (chunks) of "a length line (in hex) + the data + a line break", and a zero-length chunk marks the end. When does the length become unknown? If the CONTENT_LENGTH filter receives everything up to EOS in its first call, it counts the length and adds Content-Length; but if it has to pass something down before EOS (because it got a FLUSH, or while reading a bucket of unknown length), it can't add the length (server/protocol.c lines 1784–1937). Then, for an HTTP/1.1 request whose connection can stay alive (keep-alive), r->chunked is switched on (modules/http/http_protocol.c lines 269–270), and HTTP1_RESPONSE_OUT plugs in the CHUNK filter after sending the headers down first.

                    if (ctx->final_response_sent && r->chunked) {
                        /* We can't add this filter until we have already sent the headers.
                         * If we add it before this point, then the headers will be chunked
                         * as well, and that is just wrong.
                         */
                        rv = ap_pass_brigade(f->next, b);
                        apr_brigade_cleanup(b);
                        /* ... */
                        ap_add_output_filter("CHUNK", NULL, r, r->connection);
                    }

modules/http/http_filters.c lines 2005–2020

This spot shows that a filter can even be plugged into the chain in the middle of a response. The CHUNK filter counts the data length in the brigade and inserts a length line at the front and a line break at the end.

            hdr_len = apr_snprintf(chunk_hdr, sizeof(chunk_hdr),
                                   "%" APR_UINT64_T_HEX_FMT CRLF, (apr_uint64_t)bytes);
            ap_xlate_proto_to_ascii(chunk_hdr, hdr_len);
            e = apr_bucket_transient_create(chunk_hdr, hdr_len,
                                            c->bucket_alloc);
            APR_BRIGADE_INSERT_HEAD(b, e);

            /*
             * Insert the end-of-chunk CRLF before an EOS or
             * FLUSH bucket, or appended to the brigade
             */
            e = apr_bucket_immortal_create(CRLF_ASCII, 2, c->bucket_alloc);

modules/http/chunk_filter.c lines 176–187

The length line lives in the function's local array chunk_hdr, so it's TRANSIENT; the line break is a string constant, so it's IMMORTAL. When it meets EOS, it inserts the end marker 0\r\n\r\n (5 bytes) as an IMMORTAL bucket in front of EOS (modules/http/http_protocol.c lines 1627–1631). The bucket kinds from the table in section 1 are each chosen to fit their use.

Example 4 — when a handler writes with ap_rputs(), and chunked

The example module's handler calls ap_rputs("hello, ", r); ap_rputs("buckets\n", r);, and if the URL has ?flush, it calls ap_rflush(r) in between and writes one more line.

    ap_set_content_type(r, "text/plain");
    ap_rputs("hello, ", r);
    ap_rputs("buckets\n", r);
    if (r->args && !strcmp(r->args, "flush")) {
        ap_rflush(r);
        ap_rputs("after flush\n", r);
    }

First, the result without ?flush.

$ curl http://127.0.0.1:8080/demo
hello, buckets
--- error_log
chain: bucketlog_top -> byterange -> content_length -> http_header -> http_outerror -> http1_response_out -> bucketlog_net -> core
bucketlog_top #1: HEAP(15) EOS(0)
bucketlog_net #1: HEAP(135) HEAP(15) EOS(0)
bucketlog_net #2: EOR(0)
bucketlog_net #3: FLUSH(0) EOC(0)

ap_rputs() was called twice, yet there's a single HEAP(15), and it came down together with EOS after the handler returned. ap_rputs() doesn't call the filters right away; it plugs a filter called OLD_WRITE in at the very front of the chain and collects the text in a brigade inside it (server/protocol.c lines 2036–2067). The collecting function, apr_brigade_write(), appends to the last bucket if it is a HEAP with room left (APR-util buckets/apr_brigade.c lines 429–486). The first HEAP it creates is APR_BUCKET_BUFF_SIZE (8000 bytes, line 54) in size. When the handler returns, ap_finalize_request_protocol() sends EOS (server/protocol.c lines 1645–1653), and OLD_WRITE puts what it collected in front of it and sends it down. Since everything up to EOS came at once, CONTENT_LENGTH could count the length, and the response headers got Content-Length: 15 (checked with curl -i).

This time I added ?flush and fetched with curl --raw, which leaves the chunked encoding undecoded.

$ curl --raw http://127.0.0.1:8080/demo?flush
f
hello, buckets

c
after flush

0

--- error_log
chain: bucketlog_top -> byterange -> content_length -> http_header -> http_outerror -> http1_response_out -> bucketlog_net -> core
bucketlog_top #1: HEAP(15) FLUSH(0)
bucketlog_net #1: HEAP(143)
bucketlog_net #2: TRANSIENT(3) HEAP(15) IMMORTAL(2) FLUSH(0)
bucketlog_net #3:
bucketlog_top #2: HEAP(12) EOS(0)
bucketlog_net #4: TRANSIENT(3) HEAP(12) IMMORTAL(2) IMMORTAL(5) EOS(0)
bucketlog_net #5: EOR(0)
bucketlog_net #6: FLUSH(0) EOC(0)
  1. When ap_rflush() sent a FLUSH bucket, the collected HEAP(15) came down together with the FLUSH (top #1). There's no EOS yet, so the length is unknown.
  2. net #1 — only the 143 bytes of headers came down first. This is the "send the headers first, then plug in CHUNK" spot in the code above. The response headers had Transfer-Encoding: chunked instead of Content-Length.
  3. net #2 — TRANSIENT(3) is f\r\n (15 in hex), HEAP(15) is the body and IMMORTAL(2) is \r\n. It matches the first chunk of curl's output exactly.
  4. net #3 — an empty brigade passed through once. The CONTENT_LENGTH filter first passes everything up to the FLUSH, then at the end of the function passes the remaining brigade (now empty) once more (server/protocol.c lines 1936–1937).
  5. net #4 — the second chunk c\r\n + 12 bytes + \r\n, and IMMORTAL(5) is the end marker 0\r\n\r\n.

The brigades that bucketlog_net at the bottom saw for the ?flush request, laid out bucket by bucket. net #1 is the response header alone, HEAP(143), which contains Transfer-Encoding: chunked; the CHUNK filter is added after it. net #2 is the first chunk: TRANSIENT(3), the length line f CR LF (15 in hex), HEAP(15), the body hello, buckets LF, IMMORTAL(2), the line break CR LF, and the FLUSH(0) sent by ap_rflush. net #4 is the second chunk: TRANSIENT(3) c CR LF (12 in hex), HEAP(12) after flush LF, IMMORTAL(2) CR LF, the end marker IMMORTAL(5) 0 CR LF CR LF, and EOS(0) The net #1, #2 and #4 lines of the error_log above, one box per bucket, with the bytes each one actually holds (net #3, an empty brigade, is left out). Blue boxes were inserted by the CHUNK filter; yellow boxes are the body the handler wrote.

With ?big, the handler finally writes 10000 bytes in one go with ap_rwrite(). That's more than the room left in the existing HEAP (8000 − 15), so apr_brigade_write() doesn't copy the data; it wraps it in a TRANSIENT bucket and passes it on immediately (lines 455–459).

$ curl -o /dev/null http://127.0.0.1:8080/demo?big
--- error_log (only two lines copied here)
bucketlog_top #1: HEAP(15) TRANSIENT(10000)
bucketlog_net #2: TRANSIENT(6) HEAP(15) TRANSIENT(10000) IMMORTAL(2)

TRANSIENT(6) is 271f\r\n (10015 in hex). Finally, a handler that hands over a pipe as a PIPE bucket (? means the length is unknown). The change from the figure in section 3 shows up exactly.

$ curl http://127.0.0.1:8080/pipe
data from a pipe
--- error_log
chain: bucketlog_top -> byterange -> content_length -> http_header -> http_outerror -> http1_response_out -> bucketlog_net -> core
bucketlog_top #1: PIPE(?) EOS(0)
bucketlog_net #1: HEAP(143)
bucketlog_net #2: TRANSIENT(4) HEAP(17) IMMORTAL(2)
bucketlog_net #3: IMMORTAL(0) IMMORTAL(5) EOS(0)
bucketlog_net #4: EOR(0)
bucketlog_net #5: FLUSH(0) EOC(0)

When the CONTENT_LENGTH filter read the PIPE, it split into HEAP(17) and a new PIPE, and because that HEAP had to be passed down first, the length couldn't be fixed and the response became chunked. On the next read the pipe was finished, and the PIPE became IMMORTAL(0).

Easy to get wrong: one FLUSH changes the response format

A single ?flush turned a Content-Length response into a chunked one. From the handler's point of view it only asked "send this out quickly", but at that moment the total length is unknown, so in HTTP/1.1 chunked is the only option. The same goes for writing filters: if a content-changing filter sends pieces down before EOS, the CONTENT_LENGTH behind it can't write a length.

7. The input side — a chain that pulls

The input filters, through which the request body comes in, use the same buckets and the same six types but run the other way. Output is pushed down from above (ap_pass_brigade); for input, the upper side asks the lower side for data (ap_get_brigade).

AP_DECLARE(apr_status_t) ap_get_brigade(ap_filter_t *next,
                                        apr_bucket_brigade *bb,
                                        ap_input_mode_t mode,
                                        apr_read_type_e block,
                                        apr_off_t readbytes)
{
    if (next) {
        return next->frec->filter_func.in_func(next, bb, mode, block,
                                               readbytes);
    }
    return AP_NOBODY_READ;
}

server/util_filter.c lines 738–749

There are three more arguments. block says whether to wait when no data is available (APR_BLOCK_READ) or not (APR_NONBLOCK_READ), readbytes is the maximum number of bytes, and mode says how to read (include/util_filter.h lines 41–63).

Mode Meaning Used for
AP_MODE_GETLINE One line (up to CRLF) Reading the request line and headers (server/protocol.c line 248)
AP_MODE_READBYTES Up to readbytes bytes Reading the body (ap_get_client_block(), modules/http/http_filters.c lines 1650–1651)
AP_MODE_SPECULATIVE Read but leave it unconsumed (peek) Checking whether the next request has arrived (server/protocol.c line 418)
AP_MODE_EATCRLF Throw away leading blank lines Blank lines left after a request (per the core's comment). I found no caller using this mode at this commit
AP_MODE_EXHAUSTIVE Everything to the end The source comment warns "use with extreme caution"
AP_MODE_INIT Just initialise the connection The TLS handshake (mod_ssl.c line 731)

The CORE_IN filter at the bottom (ap_core_input_filter()) puts one SOCKET bucket into its own brigade the first time it is called (server/core_filters.c lines 118–124; the bucket is created in server/core.c lines 5898–5905). From there it's just like the PIPE in section 3: reading the socket bucket turns what was read into a HEAP and a new SOCKET bucket is attached behind it. For GETLINE it cuts one line out of that brigade and returns it (apr_brigade_split_line()); for READBYTES it cuts at the requested length. Whatever is left it keeps for the next call. Reading the request line and headers through this chain was covered in Part 9, and mod_ssl sits in the middle of this chain turning ciphertext into plaintext (Part 15).

The input filter chain. Output is pushed down from top to bottom with ap_pass_brigade(f->next, bb); for input, the upper filter asks the lower one with ap_get_brigade(next, bb, mode, block, readbytes). The request line and headers are read with AP_MODE_GETLINE, the body with AP_MODE_READBYTES, and peeking for the next request uses AP_MODE_SPECULATIVE. The call goes through the input filters in between (for example mod_ssl, which turns ciphertext into plaintext) down to CORE_IN (ap_core_input_filter()) at the bottom. CORE_IN's brigade starts with a single SOCKET bucket; reading it turns it into a HEAP of what was read plus a new SOCKET. GETLINE cuts off one line and READBYTES cuts off the requested length to return, and the rest is kept for the next call A SOCKET bucket splitting into a HEAP and a new SOCKET as it is read has the same shape as the PIPE in section 3's figure.

What this post leaves out

These are named only, for the sake of the main path. The allocator that reuses bucket memory, apr_bucket_alloc_t (APR-util buckets/apr_buckets_alloc.c). Reference counting when several buckets share the same data (apr_bucket_refcount, buckets/apr_buckets_refcount.c). mod_filter's FilterDeclare/FilterProvider, which pick filters by condition (ap_filter_provider_t). Filter init functions (filter_init_func, server/config.c lines 375–387). ap_remove_output_filter(), which removes a filter, and get_spare()/put_spare(), which reuse spent filter structs within a connection (server/util_filter.c lines 333–368). The SUBREQ_CORE filter that receives a subrequest's output. The ERROR bucket that carries an error status, and the HTTP_OUTERROR filter. The BYTERANGE filter, which cuts Range requests, comes up in Part 12. And trunk's new input filters HTTP1_REQUEST_IN and HTTP1_BODY_IN, which read HTTP/1.x requests into buckets (modules/http/http_core.c lines 372–377).

Recap

Concept One-line summary
Bucket A struct recording "where the data is". Its kind is the function pointer table apr_bucket_type_t
Data / metadata buckets Either point at data, like HEAP, FILE and PIPE, or carry only a signal, like EOS, FLUSH, EOR and EOC
Brigade A ring of buckets. Back at the head (sentinel) means the end. Joining and cutting involve no copying
Morphing buckets PIPE, SOCKET and FILE turn into HEAP, MMAP and so on when read. Nothing is read until needed
Filter A function that receives a brigade, does its work and hands it on with ap_pass_brigade(). Called many times per response
Filter types RESOURCE 10 · CONTENT_SET 20 · PROTOCOL 30 · TRANSCODE 40 · CONNECTION 50 · NETWORK 60. Lined up by number
Plugging in SetOutputFilter, the insert_filter hook, or ap_add_output_filter() in code. Request filters and connection filters live for different lengths of time
Core output filter Non-blocking writev/sendfile. What can't be sent is set aside; if one of three rules applies, it waits and sends
Chunked Used when the length is unknown and the headers must go first. The CHUNK filter is plugged in after the headers are sent
Input filters A chain that asks the one below with ap_get_brigade(). The mode picks one line, some bytes or a peek

What to look at next

We followed the path a response takes, in buckets, along the filter chain to the socket. The example also confirmed that a static file sets out as a single FILE bucket.

The next post looks at what created that FILE bucket: the handler. default_handler(), which turns a file into a response, mod_mime, which picks the Content-Type from the extension, DirectoryIndex and the trailing-slash redirect, and where 304 Not Modified and Range requests are handled — it continues in Part 12: Handlers and Static Files — How a File Becomes a Response.

How Spring solved the same problem. A servlet filter's doFilter() is called once per request; inside it, it calls chain.doFilter() to leave the rest to the chain, and when that returns the response is finished — a "wrapping" structure (Spring Part 20). httpd's filters are called many times, once for each piece (brigade) of the body, and output and input line up separately — a "flowing" structure, so stream work such as compression or chunked encoding fits naturally into a single filter. Treating the body as a flow of byte chunks is actually closer to WebFlux (Spring Part 24).

nginx, instead of plugging filters in per request, sends every request through a single chain fixed at configure time (Digging into nginx's Source 10: Filters and Static Files).

More posts