Writing a Filter

Suppose you wish to trace each request to the Micronaut “Hello World” example using some external system. This system could be a database or a distributed tracing service, and may require I/O operations.

You should not block the underlying Netty event loop in your filter; instead the filter should proceed with execution once any I/O is complete.

As an example, consider this TraceService that uses Project Reactor to compose an I/O operation:

A TraceService Example using Reactive Streams

  1. import io.micronaut.http.HttpRequest;
  2. import org.reactivestreams.Publisher;
  3. import org.slf4j.Logger;
  4. import org.slf4j.LoggerFactory;
  5. import jakarta.inject.Singleton;
  6. import reactor.core.publisher.Mono;
  7. import reactor.core.scheduler.Schedulers;
  8. @Singleton
  9. public class TraceService {
  10. private static final Logger LOG = LoggerFactory.getLogger(TraceService.class);
  11. Publisher<Boolean> trace(HttpRequest<?> request) {
  12. return Mono.fromCallable(() -> { (1)
  13. LOG.debug("Tracing request: {}", request.getUri());
  14. // trace logic here, potentially performing I/O (2)
  15. return true;
  16. }).subscribeOn(Schedulers.boundedElastic()) (3)
  17. .flux();
  18. }
  19. }

A TraceService Example using Reactive Streams

  1. import io.micronaut.http.HttpRequest
  2. import org.slf4j.Logger
  3. import org.slf4j.LoggerFactory
  4. import jakarta.inject.Singleton
  5. import reactor.core.publisher.Flux
  6. import reactor.core.publisher.Mono
  7. import reactor.core.scheduler.Schedulers
  8. import java.util.concurrent.Callable
  9. @Singleton
  10. class TraceService {
  11. private static final Logger LOG = LoggerFactory.getLogger(TraceService.class)
  12. Flux<Boolean> trace(HttpRequest<?> request) {
  13. Mono.fromCallable(() -> { (1)
  14. LOG.debug('Tracing request: {}', request.uri)
  15. // trace logic here, potentially performing I/O (2)
  16. return true
  17. }).flux().subscribeOn(Schedulers.boundedElastic()) (3)
  18. }
  19. }

A TraceService Example using Reactive Streams

  1. import io.micronaut.http.HttpRequest
  2. import org.slf4j.LoggerFactory
  3. import jakarta.inject.Singleton
  4. import reactor.core.publisher.Flux
  5. import reactor.core.publisher.Mono
  6. import reactor.core.scheduler.Schedulers
  7. @Singleton
  8. class TraceService {
  9. private val LOG = LoggerFactory.getLogger(TraceService::class.java)
  10. internal fun trace(request: HttpRequest<*>): Flux<Boolean> {
  11. return Mono.fromCallable {
  12. (1)
  13. LOG.debug("Tracing request: {}", request.uri)
  14. // trace logic here, potentially performing I/O (2)
  15. true
  16. }.subscribeOn(Schedulers.boundedElastic()) (3)
  17. .flux()
  18. }
  19. }
1The reactor:Mono[] type creates logic that executes potentially blocking operations to write the trace data from the request
2Since this is just an example, the logic does nothing yet
3The Schedulers.boundedElastic executes the logic

You can then inject this implementation into your filter definition:

An Example HttpServerFilter

  1. import io.micronaut.http.HttpRequest;
  2. import io.micronaut.http.MutableHttpResponse;
  3. import io.micronaut.http.annotation.Filter;
  4. import io.micronaut.http.filter.HttpServerFilter;
  5. import io.micronaut.http.filter.ServerFilterChain;
  6. import org.reactivestreams.Publisher;
  7. import reactor.core.publisher.Flux;
  8. @Filter("/hello/**") (1)
  9. public class TraceFilter implements HttpServerFilter { (2)
  10. private final TraceService traceService;
  11. public TraceFilter(TraceService traceService) { (3)
  12. this.traceService = traceService;
  13. }
  14. }

An Example HttpServerFilter

  1. import io.micronaut.http.HttpRequest
  2. import io.micronaut.http.MutableHttpResponse
  3. import io.micronaut.http.annotation.Filter
  4. import io.micronaut.http.filter.HttpServerFilter
  5. import io.micronaut.http.filter.ServerFilterChain
  6. import org.reactivestreams.Publisher
  7. @Filter("/hello/**") (1)
  8. class TraceFilter implements HttpServerFilter { (2)
  9. private final TraceService traceService
  10. TraceFilter(TraceService traceService) { (3)
  11. this.traceService = traceService
  12. }
  13. }

An Example HttpServerFilter

  1. import io.micronaut.http.HttpRequest
  2. import io.micronaut.http.MutableHttpResponse
  3. import io.micronaut.http.annotation.Filter
  4. import io.micronaut.http.filter.HttpServerFilter
  5. import io.micronaut.http.filter.ServerFilterChain
  6. import org.reactivestreams.Publisher
  7. @Filter("/hello/**") (1)
  8. class TraceFilter((2)
  9. private val traceService: TraceService)(3)
  10. : HttpServerFilter {
  11. }
1The Filter annotation defines the URI pattern(s) the filter matches
2The class implements the HttpServerFilter interface
3The previously defined TraceService is injected via constructor

The final step is to write the doFilter implementation of the HttpServerFilter interface.

The doFilter implementation

  1. @Override
  2. public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request,
  3. ServerFilterChain chain) {
  4. return Flux.from(traceService
  5. .trace(request)) (1)
  6. .switchMap(aBoolean -> chain.proceed(request)) (2)
  7. .doOnNext(res ->
  8. res.getHeaders().add("X-Trace-Enabled", "true") (3)
  9. );
  10. }

The doFilter implementation

  1. @Override
  2. Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request,
  3. ServerFilterChain chain) {
  4. traceService
  5. .trace(request) (1)
  6. .switchMap({ aBoolean -> chain.proceed(request) }) (2)
  7. .doOnNext({ res ->
  8. res.headers.add("X-Trace-Enabled", "true") (3)
  9. })
  10. }

The doFilter implementation

  1. override fun doFilter(request: HttpRequest<*>,
  2. chain: ServerFilterChain): Publisher<MutableHttpResponse<*>> {
  3. return traceService.trace(request) (1)
  4. .switchMap { aBoolean -> chain.proceed(request) } (2)
  5. .doOnNext { res ->
  6. res.headers.add("X-Trace-Enabled", "true") (3)
  7. }
  8. }
1TraceService is invoked to trace the request
2If the call succeeds, the filter resumes request processing using Project Reactor‘s switchMap method, which invokes the proceed method of the ServerFilterChain
3Finally, the Project Reactor‘s doOnNext method adds a X-Trace-Enabled header to the response.

The previous example demonstrates some key concepts such as executing logic in a non-blocking manner before proceeding with the request and modifying the outgoing response.

The examples use Project Reactor, however you can use any reactive framework that supports the Reactive streams specifications

The Filter annotation uses AntPathMatcher for path matching. The mapping matches URLs using the following rules:

  • ? matches one character

  • * matches zero or more characters

  • ** matches zero or more subdirectories in a path

Table 1. @Fitler Annotation Path Matching Examples
PatternExample Matched Paths

/

any path

customer/j?y

customer/joy, customer/jay

customer/*/id

customer/adam/id, com/amy/id

customer/

customer/adam, customer/adam/id, customer/adam/name

customer/*/.html

customer/index.html, customer/adam/profile.html, customer/adam/job/description.html

== Error States

The publisher returned from chain.proceed should never emit an error. In the cases where an upstream filter emitted an error or the route itself threw an exception, the error response should be emitted instead of the exception. In some cases it may be desirable to know the cause of the error response and for this purpose an attribute exists on the response if it was created as a result of an exception being emitted or thrown. The original cause is stored as the attribute EXCEPTION.