1. Overview
In this tutorial, we’ll go through the annotated HTTP filters the Micronaut framework provides. Initially, HTTP filters in Micronaut were closer to the Java EE Filter interface and the Spring Boot filters approach. But with the latest major version released, filters can now be annotation-based, separating filters for requests and responses.
In this tutorial, we’ll examine HTTP filters in Micronaut. More specifically, we’ll focus on the server filters introduced in version 4, the annotation-based filter methods.
2. HTTP Filters
HTTP filters were introduced as an interface in Java EE. It is a “specification” implemented in all Java web frameworks. As documented:
A filter is an object that performs filtering tasks on either the request to a resource (a servlet or static content), or on the response from a resource, or both.
Filters that implement the Java EE interface have a doFilter() method with 3 parameters, ServletRequest, ServletResponse, and FilterChain. This gives us access to the request object, and the response, and using the chain we pass the request and response to the next component. To this day, even newer frameworks still might use the same or similar names and parameters.
Some common real-life use cases that filters are very handy for:
- Authentication filters
- Header filter (to retrieve a value from a request or add a value in the response)
- Metrics filters (eg when recording the request execution time)
- Logging filters
3. HTTP Filters in Micronaut
HTTP filters in Micronaut follow in some way the Java EE Filter specs. For example, Micronaut’s HttpFilter interface provides a doFilter() method with a parameter for the request object and one for the chain object. The request parameter allows us to filter the request, and then use the chain object to process it and get back the response. Finally, changes can be made to the response object, if needed.
In Micronaut 4, some new annotations for filters were introduced, that offered filter methods for requests only, response only, or both.
Micronaut offers filters for our server requests received and responses sent, using the @ServerFilter. But it also offers filters for our REST clients, for requests against 3rd systems and microservices, using the @ClientFilter.
The Server filters have some concepts that make them very agile and useful:
- accept some pattern to match the path we want to filter
- can be ordered because some filters need to be executed before others (eg an authentication checking filter should be always first)
- give options about filtering responses that might be in the type of error (like filtering throwables)
We’ll get into more detail about some of those concepts in the upcoming paragraphs.
4. Filter Patterns
HTTP filters in Micronaut are specific to endpoints based on their paths. To configure which endpoint a filter is applied to, we can set a pattern to match the path. The pattern can be of different styles, like ANT or REGEX, and the value, which is the actual pattern, like /endpoint*.
There are different options for pattern style, but the default is AntPathMatcher because it is more efficient performance-wise. Regex is a more powerful style to use when using patterns to match, but it is much slower than Ant. So we should use it only as a last option when Ant doesn’t support the style we’re looking for.
Some examples of styles we’ll need when using filters are:
- /** will match any path
- /filters-annotations/** will match all paths under `filters-annotations`, like /filters-annotations/endpoint1 and /filters-annotations/endpoint2
- /filters-annotations/*1 will match all paths under `filters-annotations` but only when ending in ‘1’
- **/endpoint1 will match all paths that end in ‘endpoint1’
- **/endpoint* will match all paths that end with ‘endpoint’ plus anything extra at the end
where, in the default FilterPatternStyle.ANT style:
- * matches zero or more characters
- ** matches zero or more subdirectories in a path
5. Annotation-Based Server Filters in Micronaut
Annotated HTTP filters in Micronaut were added in Micronaut major version 4 and are also referred to as filter methods. Filter methods allow us to separate the specific filters for requests or responses. Before annotation-based filters, we only had one way to define filters and it was the same for filtering a request or a response. This way we can separate concerns, so keep our code cleaner and more readable.
Filter methods still allow us to define a filter that is both accessing a request AND modifying a response, if needed, using FilterContinuation.
5.1. Filter Methods
Based on whether we want to filter the request or response, we can use the @RequestFilter or @ResponseFilter annotations. On a class level, we still need an annotation to define the filter, the @ServerFilter. The path to filter and the order of filters are defined at the class level. We also have the option to apply path patterns per filter method.
Let’s combine all this info to create a ServerFilter that has one method that filters requests and another one that filters responses:
@Slf4j
@ServerFilter(patterns = { "**/endpoint*" })
public class CustomFilter implements Ordered {
@RequestFilter
@ExecuteOn(TaskExecutors.BLOCKING)
public void filterRequest(HttpRequest<?> request) {
String customRequestHeader = request.getHeaders()
.get(CUSTOM_HEADER_KEY);
log.info("request header: {}", customRequestHeader);
}
@ResponseFilter
public void filterResponse(MutableHttpResponse<?> res) {
res.getHeaders()
.add(X_TRACE_HEADER_KEY, "true");
}
}
The filterRequest() method is annotated with @RequestFilter and accepts an HTTPRequest parameter. This gives us access to the request. Then, it reads and logs a header in the request. In a real-life example, this could be doing more, like rejecting a request based on a header value passed.
The filterResponse() method is annotated with @ResponseFilter and accepts a MutableHttpResponse parameter, which is the response object we are about to return to the client. Before we respond though, this method adds a header in the response.
Keep in mind that the request could have already been processed by another filter we have, with a lower order, and might be processed by another filter next, with a higher order. Similarly, the response might have been processed by filters with higher order and the filters with lower order will be applied after. More on that in the Filter Order paragraph.
5.2. Continuations
The filter methods are a nice feature, to keep our code nice and clean. However, there is still the requirement to have methods that filter the same request and response. Micronaut provides continuations, to handle this requirement. The annotation on the method is the same as in requests, @RequestFilter, but the parameters are different. We also still have to use the @ServerFilter annotation on the class.
One typical example of a case in which we need to get access to a request and use the value on the response is the tracing header for the distributed tracing pattern, in distributed systems. On a high level, we use a header to trace down a request, so that we know on which exactly step it failed if it returns an error. For that, we need to pass in on every request/message a ‘request-id’ or ‘trace-id’ and if the service communicates with another service, it passes the same value:
@Slf4j
@ServerFilter(patterns = { "**/endpoint*" })
@Order(1)
public class RequestIDFilter implements Ordered {
@RequestFilter
@ExecuteOn(TaskExecutors.BLOCKING)
public void filterRequestIDHeader(
HttpRequest<?> request,
FilterContinuation<MutableHttpResponse<?>> continuation
) {
String requestIdHeader = request.getHeaders().get(REQUEST_ID_HEADER_KEY);
if (requestIdHeader == null || requestIdHeader.trim().isEmpty()) {
requestIdHeader = UUID.randomUUID().toString();
log.info(
"request ID not received. Created and will return one with value: [{}]",
requestIdHeader
);
} else {
log.info("request ID received. Request ID: [{}]", requestIdHeader);
}
MutableHttpResponse<?> res = continuation.proceed();
res.getHeaders().add(REQUEST_ID_HEADER_KEY, requestIdHeader);
}
}
The filterRequestIDHeader() method is annotated with @RequestFilter and has one HttpRequest and one FilterContinuation parameter. We get access to the request from the request parameter and check if the “Request-ID” header has a value. If not, we create one and we log the value in any case.
By using the continuation.proceed() method we get access to the response object. Then, we add the same header and value of the “Request-ID” header in the response, to be propagated up to the client.
5.3. Filter Order
In many use cases, it makes sense to have specific filters executed before or after others. HTTP filters in Micronaut provide two ways to handle the ordering of filter executions. One is the @Order Annotation and the other is implementing the Ordered interface. Both are on class level.
The way the ordering works is, that we provide an int value which is the order in which the filters will be executed. For request filters, it is straightforward. Order -5 will be executed before order 2 and order 2 will be executed before order 4. For response filters, it is the opposite. Order 4 will be applied first, then order 2 and finally order -5.
When we implement the interface, we need to manually override the getOrder() method. It defaults to zero:
@Filter(patterns = { "**/*1" })
public class PrivilegedUsersEndpointFilter implements HttpServerFilter, Ordered {
// filter methods ommited
@Override
public int getOrder() {
return 3;
}
}
When we use the Annotation, we just have to set the value:
@ServerFilter(patterns = { "**/endpoint*" })
@Order(1)
public class RequestIDFilter implements Ordered {
// filter methods ommited
}
Note, that testing the combination of @Order Annotation and implementing the Ordered interface has led to misbehavior, so it’s a good practice to choose one of the two methods and apply it everywhere.
6. Conclusion
In this tutorial, we examined the concept of filters in general and the HTTP filters in Micronaut. We saw the different options provided to implement a filter and some real-life use cases. Then, we presented examples of the annotated-based filters, for request-only filters, response-only filters, and both. Last, we spent some time on key concepts like the path patterns and the orders of the filters.
As always, all the source code is available over on GitHub.