I am seeing different behaviour when running micronaut, compared with when running a JUnit test.
I have created a test controller:
package com.testing;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Controller("/test")
public class TestController {
private static final Logger log = LoggerFactory.getLogger(TestController.class);
@Get("/throw")
public Object getThrow() {
log.info("Testing throw");
throw new TestException("Testing throw");
}
}
My exception class is:
package com.testing;
public class TestException extends RuntimeException {
public TestException(String message) {
super(message);
}
}
I have created a custom record to represent an error payload:
package com.testing;
public record CustomError (int code, String message, String description) { }
And I have an ExceptionHandler implementation of:
package com.testing;
import io.micronaut.context.annotation.Requires;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Produces;
import io.micronaut.http.server.exceptions.ExceptionHandler;
import jakarta.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Produces
@Singleton
@Requires(classes = {TestException.class, ExceptionHandler.class})
public class TestExceptionHandler implements ExceptionHandler<TestException, HttpResponse<CustomError>> {
private static final Logger log = LoggerFactory.getLogger(TestExceptionHandler.class);
@Override
public HttpResponse<CustomError> handle(HttpRequest request, TestException exception) {
log.info("In handle method");
var error = new CustomError(
HttpStatus.BAD_REQUEST.getCode(),
HttpStatus.BAD_REQUEST.name(),
"Bad Request found");
return HttpResponse.badRequest().body(error);
}
}
When I run my micronaut application, the ExceptionHandler is correctly called, and I see a response of:
HTTP/1.1 400 Bad Request
date: <the date>
Content-Type: application/json
content-length: 70
connection: keep-alive
{
"code": 400,
"message": "BAD_REQUEST",
"description": "Bad Request found"
}
However, when I try and test this in a JUnit test, the client seems to throw an exception:
package com.testing;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
@MicronautTest
public class TestControllerTest {
private static final Logger log = LoggerFactory.getLogger(TestControllerTest.class);
@Inject
@Client("/test")
HttpClient client;
@Test
public void testThrow() {
log.info("Calling throw endpoint");
assertDoesNotThrow(() -> {
var response = client.toBlocking().exchange("/throw");
log.info("Response: {}, {}", response, response.body());
assertEquals(HttpStatus.BAD_REQUEST, response.getStatus());
});
}
}
The console / stacktrace starts with:
16:14:51.684 [Test worker] INFO i.m.context.env.DefaultEnvironment - Established active environments: [test]
16:14:52.734 [Test worker] INFO com.testing.TestControllerTest - Calling throw endpoint
16:14:52.873 [default-nioEventLoopGroup-1-3] INFO com.testing.TestController - Testing throw
16:14:52.891 [default-nioEventLoopGroup-1-3] INFO com.testing.TestExceptionHandler - In handle method
Unexpected exception thrown: io.micronaut.http.client.exceptions.HttpClientResponseException: BAD_REQUEST
org.opentest4j.AssertionFailedError: Unexpected exception thrown: io.micronaut.http.client.exceptions.HttpClientResponseException: BAD_REQUEST
at app//org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:152)
Is there something additional I need to do to ensure my client can receive the response correctly?
Ah. My mistake. By default the HttpClient says it throws an exception for an error status:
I can override this behaviour using the following:
# application.yml
micronaut:
http:
client:
exception-on-error-status: false
@Test
public void testThrow() {
log.info("Calling throw endpoint");
var response = client.toBlocking().exchange("/throw", String.class, String.class);
assertEquals(HttpStatus.BAD_REQUEST, response.getStatus());
log.info("Response: {}", response.body());
assertEquals("{\"code\":400,\"message\":\"BAD_REQUEST\",\"description\":\"Bad Request found\"}", response.body());
}
Or I can allow an exception to be thrown, and use code similar to the following instead:
@Test
public void testThrow() {
log.info("Calling throw endpoint");
var error = assertThrows(HttpClientResponseException.class, () -> {
client.toBlocking().exchange("/throw");
});
assertEquals(HttpStatus.BAD_REQUEST, error.getStatus());
assertEquals("BAD_REQUEST", error.getMessage());
}