How can I validate the actual Exception in a spring-webflux test?
The following worked in the old spring-web environment, but migrating to netty and spring-webflux, the MvcResult cannot be resolved anymore (NullPointerException):
#SpringBootTest
#AutoConfigureWebTestClient
public class ApiTest {
#Test
public void test() throws Exception {
webTestClient.get()
.uri("/api?test=123")
.exchange()
.expectStatus().isBadRequest()
.expectBody().consumeWith(rsp -> {
//throws NPE
Exception ex = ((MvcResult) rsp.getMockServerResult()).getResolvedException();
assertTrue(ex instanceof ResponseStatusException);
});
}
}
#RestController
public class ApiController {
#PostMapping("/api")
public String test(#RequestParam String test) {
if (test.matches("[0-9]+"))
throw new ResponseStatusException(HttpStatus.BadRequest, "Prohibited characters");
}
}
How could I still validate the real exception class?
The main purpose of the WebTestClient is to test endpoints using fluent API to verify responses. There is no magic deserialization or error handling happening but you can get access to the raw responses (status, headers, body).
In your example you will not get MvcResult or ResponseStatusException but you could get access to the raw body using rsp.getResponseBody() that would look like
{
"timestamp": "2022-05-17T17:57:07.041+00:00",
"path": "/api",
"status": 400,
"error": "Bad Request",
"requestId": "4fa648d"
}
You could use expectBody().consumeWith(rsp -> { ... }) to get access to the request and response or expectBody(String.class).value(body -> { ... })to get just body. As an alternative use some fluent API to validate result JSON.expectBody().json()or.expectBody().jsonPath()` to check specific fields only.
In addition you could still deserialize body explicitly using .expectBody(Response.class).value(body -> {...}).
Related
I'm new to TestRestTemplate and with in Spring framework in general, and I'm trying to verify if a ResponseStatusException is thrown by my controller. For example the following degenerated request:
#RestController
public class UserManagementController {
#RequestMapping(value = "/users/{id}", method = RequestMethod.PUT)
public ResponseEntity<UserDTO> updateUser(#RequestBody UserDTO userDTO, #PathVariable("id") String id){
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "user not found");
}
}
And in my test I'm using TestRestTemplate:
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserManagementComponentTest {
#Autowired
private TestRestTemplate testRestTemplate;
#Test
public void testUpdateStaleUser() {
UserDTO updateUserDTORequest = UserDTO.builder();
assertThrows(ResponseStatusException.class,
() -> testRestTemplate.exchange("/users/" + createdUserId,
HttpMethod.PUT, new HttpEntity<>(updateUserDTORequest), UserDTO.class));
}
}
I expect to get ResponseStatusException, but the tests fails with the following message:
org.opentest4j.AssertionFailedError: Unexpected exception type thrown ==> expected: <org.springframework.web.server.ResponseStatusException> but was: <org.springframework.web.client.RestClientException>
I don't understand why RestClientException is thrown.
ResponseStatusException is the exception thrown in the server side and it will be handled by the spring-mvc framework in the server internally to return the suitable HTTP error response.
While TestRestTemplate just like a client-side REST library and hence it never can catch and handle the exception that is thrown internally from the API server.
TestRestTemplate can only throw its own exception when handling the HTTP response returned from calling an API. It will delegate to its internal RestTemplate 's ResponseErrorHandler for handling the error HTTP response.
So RestClientException is thrown by the ResponseErrorHandler that you configured for the TestRestTemplate.
Actually by default , the TestRestTemplate is configured to be fault tolerant such that it behaves in a test-friendly way by not throwing exceptions such that you can asserting directly on the returned HTTP status code or payload (see this) :
ResponseEntity<String> response = testRestTemplate.exchange("/users/" + createdUserId, HttpMethod.PUT, new HttpEntity<>(updateUserDTORequest), UserDTO.class));
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
assertThat(response.getBody()).isEqualTo(xxxxx);
I would like to throw different exceptions with different Strings (giving details of the error via message) with a very specific JSON structure, something like:
[
{
"error": [
{
"statusCode": 400,
"customMessage": "xxx",
"timestamp": "time"
}
]
}
]
Is this possible to achieve with Spring Boot? I haven't found how to do it.
Create an ErrorHandler(as per your naming convention) class and extend spring class ResponseEntityExceptionHandler which receives all method exceptions. Check spring documentation for more details on this class.
In your class add handleTechincalException method as below for required exceptions.
#ExceptionHandler({ Exception.class, AbcException.class, XyzException.class })
public ResponseEntity<Object> handleTechnicalException(Exception e, WebRequest webRequest) {
// prepare response entity object as required based on type of exception
// return ResponseEntity<Object>
}
I have #ControllerAdvice annotated class, which is handling BadRequestException extends RuntimeException exception.
Now suppose that I have endpoint:
#PostMapping(value = "/createAccount")
public ResponseEntity<CreateAccountResponse> createAccount(#RequestBody #Valid CreateAccountRequest createAccountRequest) {...}
In case of unwanted scenario, endpoint throws BadRequestException (with HTTP Status 400) which constructs error JSON object as following:
{
"errorCode": 123,
"errorMessage: "Failure reason"
}
Is there any way to document case like this using Spring REST Docs ?
This is example of my approach:
#Test
public void createAccountFailExample() {
RestDocumentationResultHandler docs = document("create-acc-fail-example",
responseFields(
fieldWithPath("errorCode").type("Integer").description("Error code"),
fieldWithPath("errorMessage").type("String").description("Error message")
)
);
org.assertj.core.api.Assertions.assertThatThrownBy(() -> this.mockMvc.perform(RestDocumentationRequestBuilders.post("/createAccount")
.contextPath("/account")
.contentType(TestUtil.APPLICATION_JSON_UTF8)
.content(TestUtil.convertObjectToJsonBytes(new CreateAccountRequest("nameTest", "surnameTest"))))
.andExpect(status().isBadRequest())
.andDo(docs)).hasCause(new BadRequestException(ServiceError.SOME_FAIL_REASON));
}
In this case test passes, but no documentation (.adoc) files are created.
When I try something like this:
ResultActions resultActions = this.mockMvc.perform(RestDocumentationRequestBuilders.post("/createAccount")
.contextPath("/account")
.contentType(TestUtil.APPLICATION_JSON_UTF8)
.content(TestUtil.convertObjectToJsonBytes(new CreateAccountRequest("testName", "testSurname"))))
.andExpect(status().isBadRequest())
.andDo(docs);
test fails because NestedServletException was thrown caused by BadRequestException, and again there is no documentation created.
I managed to solve the problem following this answer. When exception handler is defined for MockMvc, my second approach works as expected.
I have a simple webservice that returns content either as json or as plain text (depending on the clients' accept http header).
Problem: if an error occurs during text/plain request, Spring somehow returns a 406 Not Acceptable. Which is kind of wrong, because spring could as well just write the error out as plain error text, and moreover should absolutely preserve the 400 error status:
#RestController
public class TestServlet {
#PostMapping(value = "/test", produces = {APPLICATION_JSON_VALUE, TEXT_PLAIN_VALUE, "text/csv"})
public Object post() {
throw new BadRequestException("bad req");
}
}
#ResponseStatus(HttpStatus.BAD_REQUEST)
public class BadRequestException extends RuntimeException {
public BadRequestException(String msg) {
super(msg);
}
}
POST request with accept=application/json:
{
"timestamp": "2018-07-30T14:26:02",
"status": 400,
"error": "Bad Request",
"message": "bad req",
"path": "/test"
}
BUT with accept=text/csv (or text/plain) shows an empty response with status 406 Not Acceptable.
I also noticed the DispatcherServlet.processDispatchResult() is called twice: first with my BadRequest exception, 2nd time with HttpMediaTypeNotAcceptableException. So clearly the rendering of my custom exception fails, but why?
The problem is the restrictive Accept header allowing only one content type as response. In case of an error, Spring MVC needs to handle the BadRequestException and produce the required content type using a registered HttpMessageConverter.
By default Spring Boot has no message converter to produce text/plain directly from any object. You may register an ObjectToStringHttpMessageConverter (as a bean should work for Spring Boot) to allow this and you will get the result of BadRequestException.toString() as response body.
I assume a similar problem for text/csv but I am not sure how your setup for CSV message conversion looks like.
The condition written in "produces" determines the media type to use for the response to be "text/csv". So For a success scenario it works fine, **
but when you go for rendering an exception with a JSON body that
becomes a problem and gives you a 406 instead.
**
in latest versions of spring framework the problem fixes, but in old versions,as mentioned in Spring JIRA comments you should remove HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE attribute from request
the code might be like this :
#RestControllerAdvice
public class ExampleControllerAdvice {
#ExceptionHandler(value = Exception.class)
public ResponseEntity<?> handleException(HttpServletRequest request, Exception e) {
request.removeAttribute(
HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
return new ResponseEntity<?>(response, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
we can handle exception in advice
#ControllerAdvice
class ExceptionHandler{
#ExceptionHandler(value = {HttpMediaTypeNotAcceptableException.class})
public ResponseEntity handleMediaTypeException(HttpMediaTypeNotAcceptableException e) {
APIErrorResponse apiErrorResponse = new APIErrorResponse();
apiErrorResponse.setErrorCode("set custom code here");
apiErrorResponse.setErrorMessage("set custom meggage here/ here we can use message from object of exception i.e e.getMessage()");
return new ResponseEntity<>(errorDetails, HttpStatus.BAD_REQUEST);
}
}
I'm using Netflix Feign to call to one operation of a Microservice A to other other operation of a Microservice B which validates a code using Spring Boot.
The operation of Microservice B throws an exception in case of the validation has been bad. Then I handled in the Microservices and return a HttpStatus.UNPROCESSABLE_ENTITY (422) like next:
#ExceptionHandler({
ValidateException.class
})
#ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
#ResponseBody
public Object validationException(final HttpServletRequest request, final validateException exception) {
log.error(exception.getMessage(), exception);
error.setErrorMessage(exception.getMessage());
error.setErrorCode(exception.getCode().toString());
return error;
}
So, when Microservice A calls to B in a interface as next:
#Headers("Content-Type: " + MediaType.APPLICATION_JSON_UTF8_VALUE)
#RequestLine("GET /other")
void otherOperation(#Param("other") String other );
#Headers("Content-Type: " + MediaType.APPLICATION_JSON_UTF8_VALUE)
#RequestLine("GET /code/validate")
Boolean validate(#Param("prefix") String prefix);
static PromotionClient connect() {
return Feign.builder()
.encoder(new GsonEncoder())
.decoder(new GsonDecoder())
.target(PromotionClient.class, Urls.SERVICE_URL.toString());
}
and the validations fails it returns a internal error 500 with next message:
{
"timestamp": "2016-08-05T09:17:49.939+0000",
"status": 500,
"error": "Internal Server Error",
"exception": "feign.FeignException",
"message": "status 422 reading Client#validate(String); content:\n{\r\n \"errorCode\" : \"VALIDATION_EXISTS\",\r\n \"errorMessage\" : \"Code already exists.\"\r\n}",
"path": "/code/validate"
}
But I need to return the same as the Microservice operation B.
Which would be the best ways or techniques to propagate Status and Exceptions through microservices using Netflix Feign?
You could use a feign ErrorDecoder
https://github.com/OpenFeign/feign/wiki/Custom-error-handling
Here is an example
public class MyErrorDecoder implements ErrorDecoder {
private final ErrorDecoder defaultErrorDecoder = new Default();
#Override
public Exception decode(String methodKey, Response response) {
if (response.status() >= 400 && response.status() <= 499) {
return new MyBadRequestException();
}
return defaultErrorDecoder.decode(methodKey, response);
}
}
For spring to pick up the ErrorDecoder you have to put it on the ApplicationContext:
#Bean
public MyErrorDecoder myErrorDecoder() {
return new MyErrorDecoder();
}
Shameless plug for a little library I did that uses reflection to dynamically rethrow checked exceptions (and unchecked if they are on the Feign interface) based on an error code returned in the body of the response.
More information on the readme :
https://github.com/coveo/feign-error-decoder
OpenFeign's FeignException doesn't bind to a specific HTTP status (i.e. doesn't use Spring's #ResponseStatus annotation), which makes Spring default to 500 whenever faced with a FeignException. That's okay because a FeignException can have numerous causes that can't be related to a particular HTTP status.
However you can change the way that Spring handles FeignExceptions. Simply define an ExceptionHandler that handles the FeignException the way you need it (see here):
#RestControllerAdvice
public class GlobalExceptionHandler {
#ExceptionHandler(FeignException.class)
public String handleFeignStatusException(FeignException e, HttpServletResponse response) {
response.setStatus(e.status());
return "feignError";
}
}
This example makes Spring return the same HTTP status that you received from Microservice B. You can go further and also return the original response body:
response.getOutputStream().write(e.content());
Write your custom exception mapper and register it. You can customize responses.
Complete example is here
public class GenericExceptionMapper implements ExceptionMapper<Throwable> {
#Override
public Response toResponse(Throwable ex) {
return Response.status(500).entity(YOUR_RETURN_OBJ_HERE).build();
}
}
Since 2017 we've created a library that does this from annotations (making it fairly easy to, just like for requests/etc, to code this up by annotations).
it basically allows you to code error handling as follows:
#ErrorHandling(codeSpecific =
{
#ErrorCodes( codes = {401}, generate = UnAuthorizedException.class),
#ErrorCodes( codes = {403}, generate = ForbiddenException.class),
#ErrorCodes( codes = {404}, generate = UnknownItemException.class),
},
defaultException = ClassLevelDefaultException.class
)
interface GitHub {
#ErrorHandling(codeSpecific =
{
#ErrorCodes( codes = {404}, generate = NonExistentRepoException.class),
#ErrorCodes( codes = {502, 503, 504}, generate = RetryAfterCertainTimeException.class),
},
defaultException = FailedToGetContributorsException.class
)
#RequestLine("GET /repos/{owner}/{repo}/contributors")
List<Contributor> contributors(#Param("owner") String owner, #Param("repo") String repo);
}
You can find it in the OpenFeign organisation:
https://github.com/OpenFeign/feign-annotation-error-decoder
disclaimer: I'm a contributor to feign and the main dev for that error decoder.
What we do is as follows:
Share common jar which contains exceptions with both microservices.
1.) In microservices A convert exception to a DTO class lets say ErrorInfo.
Which will contain all the attributes of your custom exception with a String exceptionType, which will contain exception class name.
2.) When it is received at microservice B it will be handled by ErrorDecoder in microservice B and It will try to create an exception object from exceptionType as below:
#Override
public Exception decode(String methodKey, Response response) {
ErrorInfo errorInfo = objectMapper.readValue(details, ErrorInfo.class);
Class exceptionClass;
Exception decodedException;
try {
exceptionClass = Class.forName(errorInfo.getExceptionType());
decodedException = (Exception) exceptionClass.newInstance();
return decodedException;
}
catch (ClassNotFoundException e) {
return new PlatformExecutionException(details, errorInfo);
}
return defaultErrorDecoder.decode(methodKey, response);
}