Skip to content

Filter design doc

Jonas Konrad edited this page Apr 14, 2025 · 1 revision

This document describes the design choices made in the new filter API and implementation in Micronaut HTTP server 4.

Before 4

There has been a filter API since version 1. It looks like this:

public interface HttpFilter extends Ordered {
    Publisher<? extends HttpResponse<?>> doFilter(HttpRequest<?> request, FilterChain chain);
}

The basic usage is like this:

public class MyFilter implements HttpServerFilter {
    public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request, FilterChain chain) {
        System.out.println("Request: " + request);
        return Mono.from(chain.proceed(request)).doOnSuccess(response -> {
            System.out.println("Response: " + response);
        });
    }
}

This design is very similar to that of the servlet API, except it's reactive. You do the request processing, call into a filter chain, get a response back, do the response processing, and finally return the response.

Why a new API?

While the old API was very powerful, this power was rarely needed. Most filters only modify the request or the response. Accommodating for the full power of the old API leads to bad performance due to deep call stacks and the requirement to use reactive code. We were also limited in what convenience features we could provide to filters.

The design goals of the new filter API were thus:

  1. Make use of reactive APIs optional, allow for alternate threading models (i.e. blocking code)
  2. Reduce call stack depth where possible
  3. Make filters annotation-driven with flexible argument binders (e.g. @Body)
  4. Reduce clutter
  5. Retain the power of the old API

Goal 3 is fairly simple, so I won't go into it, but it is a prerequisite for achieving the other goals as well.

The "continuation" view of filters

The main change made by the new filter API is to decompose filters into their individual pieces. A HttpFilter as shown above consists of three parts:

  1. The request processing
  2. The FilterChain.proceed call
  3. The response processing

The filter has full control in steps 1 and 3: It can update or replace the request and response. It can call proceed on another thread, at another time, or not at all (skipping step 2). It has (almost) full freedom.

In the new API, step 2 is realized through a FilterContinuation. A continuation allows calling downstream filters and controllers, and intercepting the response. FilterContinuation improves on FilterChain in that it allows for optional blocking use and more flexible response types. A blocking filter could look like this:

@RequestFilter
@ExecuteOn(BLOCKING)
public HttpResponse<?> filter(HttpRequest<?> request, FilterContinuation<HttpResponse<?>> continuation) {
    System.out.println("Request: " + request);
    HttpResponse<?> response = continuation.proceed();
    System.out.println("Response: " + response);
    return response;
}

This approach allows us to achieve design goal 1, making reactive code optional, while still maintaining the full power of the old API. The filter can still be reactive if preferred, by changing the FilterContinuation<HttpResponse<?>> to a FilterContinuation<Publisher<HttpResponse<?>>>, which will work like the old FilterChain.

Decluttering

The next step is to declutter filters. Let's say we have this simple filter:

@RequestFilter
@ExecuteOn(BLOCKING)
public HttpResponse<?> filter(HttpRequest<?> request, FilterContinuation<HttpResponse<?>> continuation) {
    System.out.println("Request: " + request);
    return continuation.proceed();
}

This pattern, where only the request is used, is very common. We can make a seemingly simple change:

@RequestFilter
@ExecuteOn(BLOCKING)
public void filter(HttpRequest<?> request) {
    System.out.println("Request: " + request);
}

If the framework sees that the request filter does not have a continuation argument, it will add a "virtual" proceed call when the filter method is done, calling any further filters and the controller. But internally, the implementation now has much more room to optimize. No longer does it have to call downstream filters recursively. To give a pseudocode example, we can go from:

Iterator<Filter> itr = filters.iterator();

HttpResponse<?> runNext() {
    if (itr.hasNext()) {
        FilterContinuation<HttpResponse<?>> continuation = this::runNext; // recursively call next filter
        return itr.next().run(request, continuation);
    } else {
        return controller.run(request);
    }
}

to this:

for (Filter filter : filters) {
    filter.run(request);
}
return controller.run(request);

This improves performance considerably, and reduces call stack depth. At the same time, this optimization is transparent to the user: They can take their filter, add the continuation to it again, and it will behave as before (though maybe slower). We've achieved goals 2 and 4, improving performance and reducing clutter, again without compromising the power of the API.

Response filters

Analogously to the request filter, let's consider this filter:

@RequestFilter
@ExecuteOn(BLOCKING)
public HttpResponse<?> filter(FilterContinuation<HttpResponse<?>> continuation) {
    HttpResponse<?> response = continuation.proceed();
    System.out.println("Response: " + response);
    return response;
}

We can take a similar decluttering approach, and simplify this to:

@ResponseFilter
@ExecuteOn(BLOCKING)
public void filter(HttpResponse<?> response) {
    System.out.println("Response: " + response);
}

To distinguish this from the request filter, this method is annotated as @ResponseFilter. But other than that, this response filter behaves just like code in a request filter after a continuation. Again, the framework can take advantage of this to shrink call stacks and improve performance.

With these two annotations, we can split our initial example:

public class MyFilter implements HttpServerFilter {
    public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request, FilterChain chain) {
        System.out.println("Request: " + request);
        return Mono.from(chain.proceed(request)).doOnSuccess(response -> {
            System.out.println("Response: " + response);
        });
    }
}

into two pieces:

@ServerFilter
public class MyFilter {
    @RequestFilter
    public void filter(HttpRequest<?> request) {
        System.out.println("Request: " + request);
    }

    @ResponseFilter
    public void filter(HttpResponse<?> response) {
        System.out.println("Response: " + response);
    }
}

This version is not only easier to read, it's also much more performant.

The thing about state

Not every old-style filter can easily be split into two methods. Sometimes, it is necessary to carry some variable between the request and the response state:

public class MyFilter implements HttpServerFilter {
    public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request, FilterChain chain) {
        long start = System.nanoTime();
        return Mono.from(chain.proceed(request)).doOnSuccess(response -> {
            long end = System.nanoTime();
            System.out.println("Request took " + (end - start) + "ns");
        });
    }
}

There are a few ways to tackle this. The simplest is to just use a single filter with a continuation, and not split up:

@RequestFilter
@ExecuteOn(BLOCKING)
public HttpResponse<?> filter(HttpRequest<?> request, FilterContinuation<HttpResponse<?>> continuation) {
    long start = System.nanoTime();
    HttpResponse<?> response = continuation.proceed();
    long end = System.nanoTime();
    System.out.println("Request took " + (end - start) + "ns");
    return response;
}

However, this reintroduces the recursion that we want to avoid. It may be more performant to use a request attribute or other mechanism to transfer the state between the request and response filter.

The thing about order

Filters can be assigned an order and will run from highest to lowest precedence.

@Order(HIGHEST_PRECEDENCE)
public class A implements HttpServerFilter {
    public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request, FilterChain chain) {
        System.out.println("Request A");
        return Mono.from(chain.proceed(request)).doOnSuccess(response -> {
            System.out.println("Response A");
        });
    }
}

@Order(LOWEST_PRECEDENCE)
public class B implements HttpServerFilter {
    public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request, FilterChain chain) {
        System.out.println("Request B");
        return Mono.from(chain.proceed(request)).doOnSuccess(response -> {
            System.out.println("Response B");
        });
    }
}

This will print:

Request A
Request B
Response B
Response A

You can see the recursion clearly there: Filter B runs "in between" the request and response processing of filter A, inside the proceed call.

Let's migrate that example to split filters.

@ServerFilter
@Order(HIGHEST_PRECEDENCE)
public class A {
    @RequestFilter
    void request() {
        System.out.println("Request A");
    }
    
    @ResponseFilter
    void response() {
        System.out.println("Response A");
    }
}

@ServerFilter
@Order(HIGHEST_PRECEDENCE)
public class B {
    @RequestFilter
    void request() {
        System.out.println("Request B");
    }
    
    @ResponseFilter
    void response() {
        System.out.println("Response B");
    }
}

As expected, the output is the same:

Request A
Request B
Response B
Response A

If you think about filters iteratively, this can be counterintuitive. The request filters run from highest precedence to lowest precedence, but for the response filters, it's actually the lowest precedence filter that outputs its message first.

This behavior makes sense if you consider how response filters came to be, as described above. A response filter is actually the same as a request filter with an implicit continuation.proceed() call at the start. So the filters with the highest precedence are, in a way, still running first; it's just that they call proceed immediately, delegating to the other filters before it's their turn.