SpringBoot capture requestBody on error - spring

I have an app that is accepting some JSON. It has a controller with #RestController and then a
#RequestMapping(value="/junk", method=RequestMethod.POST, consumes=MediaType.APPLICATION_JSON_VALUE)
public MyResponse postInfo(#RequestBody MyRequest info) {
...
}
This "works" in that it accepts JSON that is valid as per the "MyRequest" object and handles it. I am interested in capturing the times when the JSON is not valid.
Currently what happens when there's bad data is Spring seems to write something like this to stdout:
2017-04-26 13:57:40.621 WARN 13537 --- [pr-8080-exec-13] .w.s.m.s.DefaultHandlerExceptionResolver : Failed to read HTTP message: org.springframework.http.converter.HttpMessageNotReadableException: Could not read document: Unexpected character ('}' (code 125)): was expecting double-quote to start field name
at [Source: java.io.PushbackInputStream#cc73b4; line: 1, column: 1048800]
at [Source: java.io.PushbackInputStream#cc73b4; line: 1, column: 1048796] (through reference chain: com.junk.ProcessInfo["process_list"]->java.util.ArrayList[4692]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Unexpected character ('}' (code 125)): was expecting double-quote to start field name
at [Source: java.io.PushbackInputStream#cc73b4; line: 1, column: 1048800]
at [Source: java.io.PushbackInputStream#cc73b4; line: 1, column: 1048796] (through reference chain: com.junk.ProcessInfo["process_list"]->java.util.ArrayList[4692])
This is all well and good, but I want to see what the request was that was bad so I can go fix it on the source side.
I was playing with writing a "ControllerAdvisor" like
#ControllerAdvice(annotations = RestController.class)
public class ControllerAdvisor {
private static final Logger logger = LogManager.getLogger();
#ModelAttribute
public void logBody(HttpServletRequest request, HttpServletResponse response, #RequestBody String requestString) {
logger.trace("requestString" + requestString);
}
}
which seems to write out the request body of every request (not just errors) but when I use this, it seems to modify the request such that the actual controller will fail with
2017-04-26 14:28:31.432 WARN 523 --- [io-8081-exec-29] .w.s.m.s.DefaultHandlerExceptionResolver : Failed to read HTTP message: org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public com.junk.MyResponse com.junk.ProcessController.postInfo(com.junk.MyRequest)
I was also playing with Actuator, but Actuator.trace doesn't actually give me the body.
Is there any way to get the actual requestBody inside my RestController?
Edit: Here's a complete app that demonstrates the behavior:
Download example

From what the log says it's not an issue with your server side implementation. From the log I interpret it as you posted a JSON body like this:
{
myPropert: myvalue
}
when the message converter is expecting:
{
"myPropert": "myvalue"
}

Related

Problem with Kotlin inline class deserialization to JSON in tests

I'm trying to write tests for routes in a WebFlux application, but I encountered a problem. I got an error during parsing to JSON:
13:38:20.601 [parallel-1] DEBUG org.springframework.web.server.handler.ResponseStatusExceptionHandler - [248c6c90] Resolved [ServerWebInputException: "400 BAD_REQUEST "Failed to read HTTP message"; nested exception is org.springframework.core.codec.DecodingException: JSON decoding error: Cannot construct instance of `com.test.GetItemRequest` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator); nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `com.test.GetItemRequest` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)<EOL> at [Source: (org.springframework.core.io.buffer.DefaultDataBuffer$DefaultDataBufferInputStream); line: 1, column: 2]"] for HTTP POST /v1/download
I have a request that uses an inline class:
#JvmInline
value class ItemName(val rawValue: String)
data class GetItemRequest(val name: ItemName)
The test:
#Test
fun `Request is parsed successfully`() {
//...
val client = WebTestClient.bindToRouterFunction(router.eimApiRoutes()).build()
val request = """{"name":"item1"}"""
val resp = client
.post()
.uri(EimApiRouter.DOWNLOAD_PATH)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.body(Mono.just(request), String::class.java)
.exchange()
.expectStatus()
.isOk
.expectBody()
.returnResult()
.responseBody
val expectedResponse = "OK"
assertEquals(expectedResponse, String(resp!!))
}
I can work the problem around by adding a default constructor:
data class GetItemRequest(val name: ItemName) {
constructor() : this(ItemName(""))
}
When I change the type of the parameter to String, it works.
The question:
Could somebody give the real solution? Maybe there is something missing in the test web client configuration (I tried to configure the Kotlin codecs manually, but without success)?

MessageConverter issue while using RestTemplate with StreamingResponseBody

We have a REST API (server side) implemented using Spring Boot. This API is streaming a PDF file as StreamingResponseBody wrapped in ResponseEntity where content-type is given as MediaType.APPLICATION_OCTET_STREAM.
I am trying to access this API from client application with the help of RestTemplate. This client application is again a Spring Boot app. This client application is existing and this was supporting MappingJackson2HttpMessageConverter with two supportive media types so far.
application/json and application/x-www-form-urlencoded
I followed few suggestions and tried with these items
Added MediaType.APPLICATION_OCTET_STREAM to existing
MappingJackson2HttpMessageConverter
Added ByteArrayHttpMessageConverter which has a default support to MediaType.APPLICATION_OCTET_STREAM
Added ResourceHttpMessageConverter which supporting streaming response.
But with all these suggestions I was facing the following errors. At this point of time I am not really sure if is there anything that I am missing from configuration. Team, it will be of a great help really if you can redirect me to a short examples or solutions in achieving this integration.
org.springframework.web.client.RestClientException: Error while extracting response for type [interface org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody] and content type [application/octet-stream]; nested exception is org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Unexpected character ('%' (code 37)): expected a valid value (JSON String, Number, Array, Object or token 'null', 'true' or 'false'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unexpected character ('%' (code 37)): expected a valid value (JSON String, Number, Array, Object or token 'null', 'true' or 'false') at [Source: (PushbackInputStream); line: 1, column: 2]
This following error was when I tried with ByteArrayHttpMessageConverter (or) ResourceHttpMessageConverter
org.springframework.web.client.RestClientException: Could not extract response: no suitable HttpMessageConverter found for response type [interface org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody] and content type [application/octet-stream]
at org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:123) ~[spring-web-5.2.6.RELEASE.jar!/:5.2.6.RELEASE]
Updating Question with the current implementation:
This is how resttemplate bean I am creating.
#Bean
public RestTemplate restTemplate() {
final RestTemplate restTemplate = new RestTemplate(httpRequestFactory());
final List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
final MappingJackson2HttpMessageConverter converter = new
MappingJackson2HttpMessageConverter();
final List<MediaType> mediaTypes = new ArrayList<>();
mediaTypes.add(MediaType.APPLICATION_JSON);
mediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
//mediaTypes.add(MediaType.APPLICATION_OCTET_STREAM)
converter.setSupportedMediaTypes(mediaTypes);
messageConverters.add(converter);
restTemplate.setMessageConverters(messageConverters);
return restTemplate;
}
And my API client call is
ResponseEntity<StreamingResponseBody> response = reportRestTemplate.exchange(builder.buildAndExpand(uriParams).toUriString(),HttpMethod.GET,entity,StreamingResponseBody.class,uriParams);

How to throw and detect this exception correct

I'm using #ControllerAdvice to detect exceptions that are thrown in the application.
Trying to throw exception during creation of a class:
public void setStatus(String status) throws InvalidInputStatusException{
if(checkIfStatusIsAllowed(status)) {
this.status = status;
} else {
throw new InvalidInputStatusException();
}
}
Trying to catch the error:
#ControllerAdvice
public class RekvisisjonRESTControllerExceptionHandler {
//TODO: Add logger here!
#ExceptionHandler
public final ResponseEntity<RekvisisjonRESTErrorResponse> handleException(InvalidInputStatusException e, WebRequest request) {
//TODO: Do some logging
return new ResponseEntity<>(new RekvisisjonRESTErrorResponse(HttpStatus.BAD_REQUEST.toString(),
e.getClass().getName(),
e.getMessage(), LocalDateTime.now()), HttpStatus.BAD_REQUEST);
}
}
What I want is the object specified above returned, but I get this crap here instead:
"error": "Bad Request",
"message": "JSON parse error: Ugyldig status som input; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Ugyldig status som input\n at [Source: (PushbackInputStream); line: 2, column: 12] (through reference chain: no.pasientreiser.atom.rekvisisjon.controller.dto.UpdateRekvisisjonStatusRequest[\"status\"])",
"trace": "org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Ugyldig status som input; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Ugyldig status som input\n at [Source: (PushbackInputStream); line: 2, column: 12] (through reference chain: no.pasientreiser.atom.rekvisisjon.controller.dto.UpdateRekvisisjonStatusRequest[\"status\"])\n\tat org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:245)\n\tat org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.read(AbstractJackson2HttpMessageConverter.java:227)\n\tat org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(AbstractMessageConverterMethodArgumentResolver.java:205)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.readWithMessageConverters(RequestResponseBodyMethodProcessor.java:158)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:131)\n\tat org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:121)\n\tat org.springframework.web.me
I'm assuming it fails to detect the intended exception because another is thrown before it, but this is not what i want.
Any recommendations?
An exception handler handles exceptions that occur in your handler methods (see https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc). The exception you see happens earlier, while Spring is trying to turn the JSON request body into an UpdateRekvisisjonStatusRequest. The Jackson JSON deserializer is invoking the setStatus method and encounters an exception, which Spring takes to mean the HTTP body is not readable (since Jackson couldn't deserialize it).
Take a look at how Spring MVC handles validation instead: https://spring.io/guides/gs/validating-form-input/
The exception happens not in your business code, but during the parsing of the request into your request presentation object. Spring Web treats any kind of exception that happened during parsing of the request as a presentation-level error, not a business-level error, hence, your exception handler doesn't get invoked.
Since you try to enforce a business rule here, I'd propose to move it out of the setter method of a presentation object and find a better place for it. E.g. put this logic inside the business entity, or one of your services, or at the very least in the controller method that accepts the request.
First your RekvisisjonRESTControllerExceptionHandler should extends from ResponseEntityExceptionHandler.
If you return ResponseEntity, it would wrap your value class (RekvisisjonRESTErrorResponse).
Here your exception is generated after the advice, when json serialized.

Spring RequestEntity<Object> needs Accept header mandatorily when the server fails

I am working with Spring Framework version 4.3.6.
It works with RestTemplate and thus offers support for XML and JSON
Reminder: about HTTP methods, for POST and PUT has sense mandatorily define the Content-Type header. It either for XML or JSON.
I have a situation for RequestEntity<Object> about testing:
I have the following creation for POST
RequestEntity<Object> requestEntity =
RequestEntity.post(uri).contentType(MediaType.APPLICATION_XML)
.header("Accept-Language", locale.toString())
.body(entity);
Until here it works fine when none exception is thrown by the server. It means, the entity is saved or persisted in the database without problem.
I have a problem when in other testing scenario the entity already exists in the database. I have a #ControllerAdvice that works through #ExceptionHandler. The server side works how is expected. It catchs the exception and generates an error message but...
...the problem only happens with XML, I get the following error message (the #Test method fails of course)
org.springframework.http.converter.HttpMessageNotReadableException: Could not read document: com.ctc.wstx.exc.WstxUnexpectedCharException: Unexpected character '{' (code 123) in prolog; expected '<'
at [row,col {unknown-source}]: [1,1]; nested exception is java.io.IOException: com.ctc.wstx.exc.WstxUnexpectedCharException: Unexpected character '{' (code 123) in prolog; expected '<'
at [row,col {unknown-source}]: [1,1]
I did realize the unique way to avoid that error message is through the following edition:
RequestEntity<Object> requestEntity =
RequestEntity.post(uri).contentType(MediaType.APPLICATION_XML)
.accept(MediaType.APPLICATION_XML)//<---- new
.header("Accept-Language", locale.toString())
.body(entity);
I am confused why it works, it because for POST is not mandatory define the Accept header. Even when in the server side I can define for the ResponseEntity<Object> (response) the Content-Type and Accept headers to XML value, the #Test fails. The unique way to solve this is adding for the RequestEntity<Object> (request) the .accept(MediaType.APPLICATION_XML) part.
Here the other problem is that we must assume other Java developers using RestTemplate are going to create the RequestEntity<Object> object with just defining the Content-Type and without defining the Accept header.
Note: it fails even when the RestTemplate uses the setErrorHandler method
Again, it only happens with XML, for JSON I have the following:
RequestEntity<Object> requestEntity =
RequestEntity.post(uri).contentType(MediaType.APPLICATION_JSON_UTF8)
.header("Accept-Language", locale.toString())
.body(entity);
and always works for valid and invalid (an entity already persisted) scenarios.
What is the best approach to around this situation?
Alpha
Even If I do the following edition how was suggested:
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.favorPathExtension(true)
.favorParameter(false)
.ignoreAcceptHeader(false)
//.defaultContentType(new MediaType(MediaType.TEXT_PLAIN, StandardCharsets.UTF_8))
.defaultContentType(new MediaType(MediaType.APPLICATION_XML, StandardCharsets.UTF_8))
.mediaType("html", MediaType.TEXT_HTML)
.mediaType("xml", new MediaType(MediaType.APPLICATION_XML, StandardCharsets.UTF_8))
.mediaType("json", MediaType.APPLICATION_JSON_UTF8);
}
I get the same situation
About XML, I don't use JAXB2 because it does not support Generic Collections, thus I use Jackson jackson-dataformat-xml
Important I only have this situation when is used RequestEntity<Object> with RestTemplate, for other testings working around
ResultActions resultActions =
mockMvc.perform(post(uri).contentType(MediaType.APPLICATION_XML)
.header("Accept-Language", locale.toString())
.content(entity)).andDo(print());
Observe, there is no a .accept(MediaType.APPLICATION_XML) sentence.
All work fine.
Application/json is probably configured as the default media type in your services application somewhere. That is why unless you specify the Accept header to be application/xml, you are getting json response.
If you want to make application/xml your default response media type, you will need to configure the ContentNegotiationManagerFactoryBean (Maybe you are already doing this but for application/json). Something like this:
#Configuration
#EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
/**
* Setup a simple strategy: use all the defaults and return XML by default when not sure.
*/
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.defaultContentType(MediaType.APPLICATION_XML);
}
}
You can find this in spring documentation here.
The other thing you need to look at is the entity you are using in error scenarios. If it is properly annotated with Jaxb annotations (#XmlRootElement)

How to handle HttpMediaTypeNotAcceptableException by writing error content to the response body using exception handler annotation?

When a client request for a resource producing application/json content with Accept Header of application/xml. The request fails with HttpMediaTypeNotAcceptableException exception and is wrapped into error message body in the response entity object by using exception handler annotation as mentioned in below code. However, we receive HttpMediaTypeNotAcceptableException again when return values are written to the response with HttpMessageConverter. It is because it checks the producible content type for the response with the acceptable request type, but this is exactly something we are trying to communicate to the client using error message. How do I workaround this issue ? Btw, all the other exceptions are parsing fine to error message. Please advise.
#ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body,
HttpHeaders headers, HttpStatus status, WebRequest request) {
// Setting the response content type to json
headers.setContentType(MediaType.APPLICATION_JSON);
return ResponseEntity.status(status).headers(headers).body(body);
}
}
A few options come to my mind. One is that your controller method produces all content types and then you throw an exception in your method if the content type is not the one you are expecting, then the exception handler can take this exception and transform it. This is the only one that works with exception handlers, as exception handlers only deal with exceptions produced in the controller method.
The other options are:
Use an interceptor (but I'm not sure if this will work, as Spring might try to resolve first the controller method rather than invoking the interceptors).
Extend RequestMappingHandlerMapping to call the exception handler if it doesn't find a suitable method. You'll probably need to override the method handleNoMatch. In there you'll need to get a reference to the list of HandlerExceptionResolver
The first one is the simplest to understand, and the latest one might be the most 'extensible', but it also requires some understanding of the internals of Spring.
Resolved by setting different content negotiation strategy FixedContentNegotiationStrategy for ExceptionHandlerExceptionResolver and HeaderContentNegotiationStrategy for RequestResponseBodyMethodProcessor.
I have been using a serialized enum-based response (enum annotated with jackson #JsonFormat(shape = Shape.OBJECT) to standardize the error messages in my exception handler class and faced the same issue when it caught with a HttpMediaTypeNotAcceptableException.
The workaround is to set the media type you expect to return directly to the builder method available in the ResponseEntity.
The below code works fine for me.
#ExceptionHandler(HttpMediaTypeNotAcceptableException.class)
public final ResponseEntity<ResponseMessagesEnum> handleHttpMediaTypeNotAcceptableException(
HttpMediaTypeNotAcceptableException e, HttpServletRequest request) {
logger.error("No acceptable representation found for [{}] | supported {}", request.getHeader("Accept"), e.getSupportedMediaTypes());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).contentType(MediaType.APPLICATION_JSON)
.body(ResponseMessagesEnum.EX_001);
}

Resources