How do I hook into micronaut server on error handling from a filter?

Viewed 4315

For any 4xx or 5xx response given out by my micronaut server, I'd like to log the response status code and endpoint it targeted. It looks like a filter would be a good place for this, but I can't seem to figure out how to plug into the onError handling

for instance, this filter

@Filter("/**")
class RequestLoggerFilter: OncePerRequestHttpServerFilter() {
  companion object {
    private val log = LogManager.getLogger(RequestLoggerFilter::class.java)
  }

  override fun doFilterOnce(request: HttpRequest<*>, chain: ServerFilterChain): Publisher<MutableHttpResponse<*>>? {
    return Publishers.then(chain.proceed(request), ResponseLogger(request))
  }

  class ResponseLogger(private val request: HttpRequest<*>): Consumer<MutableHttpResponse<*>> {
    override fun accept(response: MutableHttpResponse<*>) {
      log.info("Status: ${response.status.code} Endpoint: ${request.path}")
    }
  }
}

only logs on a successful response and not on 4xx or 5xx responses. How would i get this to hook into the onError handling?

3 Answers

You could do the following. Create your own ApplicationException ( extends RuntimeException), there you could handle your application errors and in particular how they result into http error codes. You exception could hold the status code as well.

Example:

class BadRequestException extends ApplicationException {
 
    public HttpStatus getStatus() {
        return HttpStatus.BAD_REQUEST;
    }
}

You could have multiple of this ExceptionHandler for different purposes.

@Slf4j
@Produces
@Singleton
@Requires(classes = {ApplicationException.class, ExceptionHandler.class})
public class ApplicationExceptionHandler implements ExceptionHandler<ApplicationException, HttpResponse> {

    @Override
    public HttpResponse handle(final HttpRequest request, final ApplicationException exception) {
        log.error("Application exception message={}, cause={}", exception.getMessage(), exception.getCause());
        final String message = exception.getMessage();
        final String code = exception.getClass().getSimpleName();
        final ErrorCode error = new ErrorCode(message, code);

        log.info("Status: ${exception.getStatus())} Endpoint: ${request.path}")

        return HttpResponse.status(exception.getStatus()).body(error);
    }
}

If you are trying to handle Micronaut native exceptions like 400 (Bad Request) produced by ConstraintExceptionHandler you will need to Replace the beans to do that.

I've posted example here how to handle ConstraintExceptionHandler.

If you want to only handle responses itself you could use this mapping each response code (example on @Controller so not sure if it works elsewhere even with global flag:

@Error(status = HttpStatus.NOT_FOUND, global = true)  
public HttpResponse notFound(HttpRequest request) {
  <...>
}

Example from Micronaut documentation.

Below code I used for adding custom cors headers in the error responses, in doOnError you can log errors

@Filter("/**")
public class ResponseCORSAdder implements HttpServerFilter {
    @Override
    public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request, ServerFilterChain chain) {
            return this.trace(request) 
            .switchMap(aBoolean -> chain.proceed(request))
            .doOnError(error -> {
                if (error instanceof MutableHttpResponse<?>) {
                    MutableHttpResponse<?> res = (MutableHttpResponse<?>) error;
                    addCorsHeaders(res);
                }
            })  
            .doOnNext(res -> addCorsHeaders(res));
    }

        private MutableHttpResponse<?> addCorsHeaders(MutableHttpResponse<?> res) {
            return res
            .header("Access-Control-Allow-Origin", "*")
            .header("Access-Control-Allow-Methods", "OPTIONS,POST,GET")
            .header("Access-Control-Allow-Credentials", "true");
        }

        private Flowable<Boolean> trace(HttpRequest<?> request) {
            return Flowable.fromCallable(() -> { 
                    // trace logic here, potentially performing I/O 
                    return true;
            }).subscribeOn(Schedulers.io()); 
        }
}
Related