How to implement an HTTP output filter in nginx

Internally nginx HTTP server has a stack of output filters. These filters affect the reply generated by other sources, such as phase handlers or upstream. There are 2 types of output filter: header filters and body filters. Obviously, header filters apply to the header of the reply and body filters apply to the body of the reply.

Header filters

Header filters adapt the reply by manipulating the status and header lines of the reply. Nginx has a structure called ngx_http_headers_out_t that stores data to be sent out in the reply header. This structure appears as headers_out in the HTTP request structure ngx_http_request_t.

Let’s take a look at the most important internals of this structure (from version 1.17.6):

typedef struct {
    ngx_list_t                        headers;
    ngx_list_t                        trailers;
    ngx_uint_t                        status;
    ngx_str_t                         status_line;
    [...]
} ngx_http_headers_out_t;

Here we see headers — which is a list of header lines to be sent, trailers — a list of trailer lines to be sent, status — is the status code of the request and status_line — is the status line of the upcoming reply. The rest of the members of this structure are omitted because they serve only specific purpose in the codebase.

Any time before the reply header is sent you can extend the headers, the trailers or change the status of the reply. This is pretty sufficient for majority of functions that a header filter can perform.

A header filter itself is a function with the following signature:

ngx_int_t header_filter(ngx_http_request_t *r);

That is, it takes a request as the only argument and returns a status code. The only status code that this function can return by itself is NGX_ERROR indicating that the an error has occurred and the header filter thinks processing of the reply cannot go on anymore. In any other case a header filter must call the next header filter in the chain.

How is the filter chain formed? Nginx codebase has a global variable ngx_http_top_header_filter. This one always points to the top header filter in the stack. Upon startup Nginx calls initialization functions for each module. Each module can capture the pointer to the top filter in the stack and replace it by an own filter. Typically the top filter is saved into a variable called ngx_http_next_header_filter by every module that wants to install a header filter:

static ngx_http_output_header_filter_pt ngx_http_next_header_filter;
static ngx_int_t my_module_init(ngx_conf_t *cf)
{
    ngx_http_next_header_filter = ngx_http_top_header_filter;
    ngx_http_top_header_filter = my_header_filter;
    return NGX_OK;
}

This variable is declared static. This way every header filter can have an own instance of this variable. By calling ngx_http_next_header_filter a header filter passes execution to the next header filter in chain.

At the bottom of the stack there is function that takes everything from headers_out, transforms this data into a sequence of bytes and calls so-called write filter that writes everything to the socket. Thus, the reply header gets sent out.

Thus, if a header filter does not pass execution to the next filter in the chain, the HTTP reply header will never gets written. This is an error.

So, what is the typical flow of a header filter? From the above it appears very simple: step 1 — check if any modifications of the reply is needed, step 2 — modify the reply, step 3 — pass execution to the next filter:

static ngx_int_t my_header_filter(ngx_http_request_t *r)
{
   if([ check if any adaptation of the reply is needed ])
   {
      [ modify the reply by adding/changing headers lines or changing status ]      
      h = ngx_list_push(&r->headers_out.headers);
      if(h == NULL) {
          return NGX_ERROR;
      }
      h->hash = 1;
      ngx_str_set(&h->key, "X-Header");
      ngx_str_set(&h->value, "x-value");
   }
   // Call the next header filter in the chain
   return ngx_http_next_header_filter(r);
}

When a header filter decides that no adaptation of the reply is needed, the execution is passed straight to the next filter and the reply remains intact.