Spring REST Controller Unsupported Media Type or No Handler - spring

If I have a spring REST controller like this
#PostMapping(
value = "/configurations",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseStatus(HttpStatus.CREATED)
public CreateConfigurationResponse createConfiguration(
#RequestBody #Valid #NotNull final CreateConfigurationRequest request) {
// do stuff
}
and a client calls this endpoint with the wrong media type in the Accept header then spring throws a HttpMediaTypeNotAcceptableException. Then our exception handler catches that and constructs a Problem (rfc-7807) error response
#Order(Ordered.HIGHEST_PRECEDENCE)
#ControllerAdvice
public class HttpMediaTypeExceptionHandler extends BaseExceptionHandler {
#ExceptionHandler(HttpMediaTypeNotAcceptableException.class)
public ResponseEntity<Problem> notAcceptableMediaTypeHandler(final HttpMediaTypeNotAcceptableException ex,
final HttpServletRequest request) {
final Problem problem = Problem.builder()
.withType(URI.create("...."))
.withTitle("unsupported media type")
.withStatus(Status.NOT_ACCEPTABLE)
.withDetail("...error stuff..")
.build();
return new ResponseEntity<>(problem, httpStatus);
}
But since the Problem error response should be sent back with a media type application/problem+json spring then sees that as not acceptable media type and calls the HttpMediaTypeExceptionHandler exception handler again and says that media type is not acceptable.
Is there a way in Spring to stop this second loop into the exception handler and even though the accept header didn't include the application/problem+json media type it will just return that anyway?

So strangely it started working when I changed the return statement from this:
return new ResponseEntity<>(problem, httpStatus);
to this:
return ResponseEntity
.status(httpStatus)
.contentType(MediaType.APPLICATION_PROBLEM_JSON)
.body(problem);
I'm not sure how this makes it work but it does.

Related

Spring Boot MVC to allow any kind of content-type in controller

I have a RestController that multiple partners use to send XML requests. However this is a legacy system that it was passed on to me and the original implementation was done in a very loose way in PHP.
This has allowed to clients, that now they refuse to change, to send different content-types (application/xml, text/xml, application/x-www-form-urlencoded) and it has left me with the need to support many MediaTypes to avoid returning 415 MediaType Not Supported Errors.
I have used the following code in a configuration class to allow many media types.
#Bean
public MarshallingHttpMessageConverter marshallingMessageConverter() {
MarshallingHttpMessageConverter converter = new MarshallingHttpMessageConverter();
converter.setMarshaller(jaxbMarshaller());
converter.setUnmarshaller(jaxbMarshaller());
converter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_XML,
MediaType.TEXT_XML, MediaType.TEXT_PLAIN, MediaType.APPLICATION_FORM_URLENCODED, MediaType.ALL));
return converter;
}
#Bean
public Jaxb2Marshaller jaxbMarshaller() {
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setClassesToBeBound(CouponIssuedStatusDTO.class, CouponIssuedFailedDTO.class,
CouponIssuedSuccessDTO.class, RedemptionSuccessResultDTO.class, RedemptionResultHeaderDTO.class,
RedemptionFailResultDTO.class, RedemptionResultBodyDTO.class, RedemptionDTO.class, Param.class,
ChannelDTO.class, RedeemRequest.class);
Map<String, Object> props = new HashMap<>();
props.put(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.setMarshallerProperties(props);
return marshaller;
}
The controller method is this:
#PostMapping(value = "/request", produces = { "application/xml;charset=UTF-8" }, consumes = MediaType.ALL_VALUE)
public ResponseEntity<RedemptionResultDTO> request(
#RequestHeader(name = "Content-Type", required = false) String contentType,
#RequestBody String redeemRequest) {
return requestCustom(contentType, redeemRequest);
}
This endpoint is hit by all clients. It is only one last client giving me trouble. They are sending content-type = application/x-www-form-urlencoded; charset=65001 (UTF-8)": 65001 (UTF-8)
Due to the way the charset is sent, Spring Boot refuses to return anything but 415. Not even MediaType.ALL seems to have any effect.
Is there a way to make Spring allow this to reach me ignoring the content-type? Creating a filter and changing the content type was not feasible since the HttpServletRequest is not allowing to mutate the content-type. I am out of ideas but I really think there has to be a way to allow custom content-types.
UPDATE
If I remove the #RequestBody then I don't get the error 415 but I have no way to get the request body since the HttpServletRequest reaches the Controller action empty.
You best case is to remove the consumes argument from the RequestMapping constructor. The moment you have it added, spring will try to parse it into known type MediaType.parseMediaType(request.getContentType()) & which tries to create a new MimeType(type, subtype, parameters) and thus throws exception due to invalid charset format being passed.
However, if you remove the consumes, and you wanna validate/restrict the incoming Content-Type to certain type, you can inject HttpServletRequest in your method as parameter, and then check the value of request.getHeader(HttpHeaders.CONTENT_TYPE).
You also have to remove the #RequestBody annotation so Spring doesn't attempt to parse the content-type in attempt to unmarshall the body. If you directly attempt to read the request.getInputStream() or request.getReader() here, you will see null as the stream has already been read by Spring. So to get access to input content, use spring's ContentCachingRequestWrapper inject using Filter and then you can later repeatedly read the content as it's cached & not reading from original stream.
I am including some code snippet here for reference, however to see executable example, you can refer my github repo. Its a spring-boot project with maven, once you launch it, you can send your post request to http://localhost:3007/badmedia & it will reflect you back in response request content-type & body. Hope this helps.
#RestController
public class BadMediaController {
#PostMapping("/badmedia")
#ResponseBody
public Object reflect(HttpServletRequest request) throws IOException {
ObjectMapper mapper = new ObjectMapper();
JsonNode rootNode = mapper.createObjectNode();
((ObjectNode) rootNode).put("contentType", request.getHeader(HttpHeaders.CONTENT_TYPE));
String body = new String(((ContentCachingRequestWrapper) request).getContentAsByteArray(), StandardCharsets.UTF_8);
body = URLDecoder.decode(body, StandardCharsets.UTF_8.name());
((ObjectNode) rootNode).put("body", body);
return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(rootNode);
}
}
#Component
public class CacheRequestFilter extends GenericFilterBean {
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest cachedRequest
= new ContentCachingRequestWrapper((HttpServletRequest) servletRequest);
//invoke caching
cachedRequest.getParameterMap();
chain.doFilter(cachedRequest, servletResponse);
}
}

Spring mvc - Configuring Error handling for XML and JSON Response

i have one REST API method :which will return Xml as response . Just for simplicity assume it throws simple Exception.
#RequestMapping(value = "machine/xmlData", method = RequestMethod.GET, produces = "application/xml")
public ResponseEntity<String> getXml(HttpServletRequest request)
throws Exception {
return getDataFromService();
}
Now i am handling the Exception in REST Controller like this.
This is generic Exception Handle method, for other API methods as well.(Xml or JSON Response)
#ExceptionHandler(Exception.class)
#ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public ResponseEntity HandleException(Exception ex, HttpServletRequest request) {
ex.printStackTrace();
// here logic to generate Custom error Object
return new ResponseEntity<Object>(customErrorObject, HttpStatus.INTERNAL_SERVER_ERROR);
}
Case 1: Accept :"application/xml" and valid Response from Service
Everything works fine.
Case 2: Accept :"application/xml" and Exception from Service
then i get 406 Not Representable
As per my understanding it is
because ResponseEntity from HandleException is JSON and accept header
is "application/xml" thats why i am getting 406.
Is there anyway that i can send the error Response from HandleException method as xml and json ?
I know on REST API methods we can define something like this produces={"application/json","application/xml"} i am struggling to put this on HandleException Method.
Any tip would be of great help.
Thanks.
You could take advantage of the spring-mvc HttpMessageConverters by using the #ResponseBody annotation( https://spring.io/blog/2013/05/11/content-negotiation-using-spring-mvc). This annotation is responsible for choosing the correct messageConverter for a given response type.
For your response to be xml or json compatible you need to do the following:
#XmlRootElement
#XmlAccessorType(XmlAccessType.FIELD)
public class WrappedExceptionResponse {
public String respone;
public String getRespone() {
return respone;
}
public void setRespone(String respone) {
this.respone = respone;
}
}
And change your exception handler method to
#ExceptionHandler(Exception.class)
#ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public #ResponseBody WrappedExceptionResponse HandleException(Exception ex, HttpServletRequest request) {
// ex.printStackTrace();
// here logic to generate Custom error Object
WrappedExceptionResponse resp=new WrappedExceptionResponse();
resp.setRespone(ex.getMessage());
return resp;
And then your exception response would be dependent on the content-type you give.

How to handle Exception occuring when returning StreamingResponseBody from RestController

I have implemented a Spring Rest Controller that streams back large files using the StreamingResponseBody. However, these files are coming from another system and there is the potential for something to go wrong while streaming them back. When this occurs I am throwing a custom Exception (MyException). I am handling the exception in an #ExceptionHandler implementation which is below. I am attempting to set the response httpstatus and error message but I am always receiving http status 406. What is the proper way to handle errors/exceptions while returning a StreamingResponseBody?
#ExceptionHandler(MyException.class)
public void handleParsException( MyException exception, HttpServletResponse response) throws IOException
{
response.sendError(HttpStatus.INTERNAL_SERVER_ERROR.value(),exception.getMessage());
}
You should handle all errors in the same way. There are many options.
I prefer next:
Controller Advice
It is a good idea to have an entity to send a generic error response, an example:
public class Error {
private String code;
private int status;
private String message;
// Getters and Setters
}
Otherwise, to handle exceptions you should create a class annotated with #ControllerAdvice and then create methods annotated with #ExceptionHandler and the exception or exceptions (it could be more than one) you want to handle. Finally return ResponseEntity<Error> with the status code you want.
public class Hanlder{
#ExceptionHandler(MyException.class)
public ResponseEntity<?> handleResourceNotFoundException(MyException
myException, HttpServletRequest request) {
Error error = new Error();
error.setStatus(HttpStatus.CONFLICT.value()); //Status you want
error.setCode("CODE");
error.setMessage(myException.getMessage());
return new ResponseEntity<>(error, null, HttpStatus.CONFLICT);
}
#ExceptionHandler({DataAccessException.class, , OtherException.class})
public ResponseEntity<?> handleResourceNotFoundException(Exception
exception, HttpServletRequest request) {
Error error = new Error();
error.setStatus(HttpStatus.INTERNAL_ERROR.value()); //Status you want
error.setCode("CODE");
error.setMessage(myException.getMessage());
return new ResponseEntity<>(error, null, HttpStatus.INTERNAL_ERROR);
}
}
Other ways:
Annotate exception directly
Other way is annotating directly the excetion with the status and the reason to return:
#ResponseStatus(value=HttpStatus.CONFLICT, reason="Error with StreamingResponseBody")
public class MyError extends RuntimeException {
// Impl ...
}
Exception Handler in a specific controller
Use a method annotated with #ExceptionHandler in a method of a #Controller to handle #RequestMapping exceptions:
#ResponseStatus(value=HttpStatus.CONFLICT,
reason="Error with StreamingResponse Body")
#ExceptionHandler(MyError.class)
public void entitiyExists() {
}
I figured the problem out. The client was only accepting the file type as an acceptable response. Therefore, when returning an error in the form of an html page I was getting httpstatus 406. I just needed to tell the client to accept html as well to display the message.

Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported for #RequestBody MultiValueMap

Based on the answer for problem with x-www-form-urlencoded with Spring #Controller
I have written the below #Controller method
#RequestMapping(value = "/{email}/authenticate", method = RequestMethod.POST
, produces = {"application/json", "application/xml"}
, consumes = {"application/x-www-form-urlencoded"}
)
public
#ResponseBody
Representation authenticate(#PathVariable("email") String anEmailAddress,
#RequestBody MultiValueMap paramMap)
throws Exception {
if(paramMap == null || paramMap.get("password") == null) {
throw new IllegalArgumentException("Password not provided");
}
}
the request to which fails with the below error
{
"timestamp": 1447911866786,
"status": 415,
"error": "Unsupported Media Type",
"exception": "org.springframework.web.HttpMediaTypeNotSupportedException",
"message": "Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported",
"path": "/users/usermail%40gmail.com/authenticate"
}
[PS: Jersey was far more friendly, but couldn't use it now given the practical restrictions here]
The problem is that when we use application/x-www-form-urlencoded, Spring doesn't understand it as a RequestBody. So, if we want to use this
we must remove the #RequestBody annotation.
Then try the following:
#RequestMapping(
path = "/{email}/authenticate",
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = {
MediaType.APPLICATION_ATOM_XML_VALUE,
MediaType.APPLICATION_JSON_VALUE
})
public #ResponseBody Representation authenticate(
#PathVariable("email") String anEmailAddress,
MultiValueMap paramMap) throws Exception {
if (paramMap == null &&
paramMap.get("password") == null) {
throw new IllegalArgumentException("Password not provided");
}
return null;
}
Note that removed the annotation #RequestBody
answer: Http Post request with content type application/x-www-form-urlencoded not working in Spring
It seems that now you can just mark the method parameter with #RequestParam and it will do the job for you.
#PostMapping( "some/request/path" )
public void someControllerMethod( #RequestParam Map<String, String> body ) {
//work with Map
}
Add a header to your request to set content type to application/json
curl -H 'Content-Type: application/json' -s -XPOST http://your.domain.com/ -d YOUR_JSON_BODY
this way spring knows how to parse the content.
In Spring 5
#PostMapping( "some/request/path" )
public void someControllerMethod( #RequestParam MultiValueMap body ) {
// import org.springframework.util.MultiValueMap;
String datax = (String) body .getFirst("datax");
}
#RequestBody MultiValueMap paramMap
in here Remove the #RequestBody Annotaion
#RequestMapping(value = "/signin",method = RequestMethod.POST)
public String createAccount(#RequestBody LogingData user){
logingService.save(user);
return "login";
}
#RequestMapping(value = "/signin",method = RequestMethod.POST)
public String createAccount( LogingData user){
logingService.save(user);
return "login";
}
like that
Simply removing #RequestBody annotation solves the problem (tested on Spring Boot 2):
#RestController
public class MyController {
#PostMapping
public void method(#Valid RequestDto dto) {
// method body ...
}
}
I met the same problem when I want to process my simple HTML form submission (without using thymeleaf or Spring's form tag) in Spring MVC.
The answer of Douglas Ribeiro will work very well. But just in case, for anyone, like me, who really want to use "#RequestBody" in Spring MVC.
Here is the cause of the problem:
Spring need to ① recognize the "Content-Type", and ② convert the
content to the parameter type we declared in the method's signature.
The 'application/x-www-form-urlencoded' is not supported, because, by
default, the Spring cannot find a proper HttpMessageConverter to do
the converting job, which is step ②.
Solution:
We manually add a proper HttpMessageConverter into the Spring's
configuration of our application.
Steps:
Choose the HttpMessageConverter's class we want to use. For
'application/x-www-form-urlencoded', we can choose
"org.springframework.http.converter.FormHttpMessageConverter".
Add the FormHttpMessageConverter object to Spring's configuration,
by calling the "public void
configureMessageConverters(List<HttpMessageConverter<?>>
converters)" method of the "WebMvcConfigurer" implementation class
in our application. Inside the method, we can add any
HttpMessageConverter object as needed, by using "converters.add()".
By the way, the reason why we can access the value by using "#RequestParam" is:
According to Servlet Specification (Section 3.1.1):
The following are the conditions that must be met before post form
data will be populated to the parameter set: The request is an HTTP
or HTTPS request. 2. The HTTP method is POST. 3. The content type is
application/x-www-form-urlencoded. 4. The servlet has made an initial
call of any of the getParameter family of methods on the request
object.
So, the value in request body will be populated to parameters. But in Spring, you can still access RequestBody, even you can use #RequstBody and #RequestParam at the same method's signature.
Like:
#RequestMapping(method = RequestMethod.POST, consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE})
public String processForm(#RequestParam Map<String, String> inputValue, #RequestBody MultiValueMap<String, List<String>> formInfo) {
......
......
}
The inputValue and formInfo contains the same data, excpet for the type for "#RequestParam" is Map, while for "#RequestBody" is MultiValueMap.
I wrote about an alternative in this StackOverflow answer.
There I wrote step by step, explaining with code. The short way:
First: write an object
Second: create a converter to mapping the model extending the AbstractHttpMessageConverter
Third: tell to spring use this converter implementing a WebMvcConfigurer.class overriding the configureMessageConverters method
Fourth and final: using this implementation setting in the mapping inside your controller the consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE and #RequestBody in front of your object.
I'm using spring boot 2.
#PostMapping(path = "/my/endpoint", consumes = { MediaType.APPLICATION_FORM_URLENCODED_VALUE })
public ResponseEntity<Void> handleBrowserSubmissions(MyDTO dto) throws Exception {
...
}
That way works for me
You can try to turn support on in spring's converter
#EnableWebMvc
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// add converter suport Content-Type: 'application/x-www-form-urlencoded'
converters.stream()
.filter(AllEncompassingFormHttpMessageConverter.class::isInstance)
.map(AllEncompassingFormHttpMessageConverter.class::cast)
.findFirst()
.ifPresent(converter -> converter.addSupportedMediaTypes(MediaType.APPLICATION_FORM_URLENCODED_VALUE));
}
}
Just add an HTTP Header Manager if you are testing using JMeter :

How to get the HTTP Request body content in a Spring Boot Filter?

I want to get the raw content that is posted towards a RestController. I need it to do some processing on the raw input.
How can I get the raw body content without interfering with the Filter Chain?
Here is a sample of controllerAdvice where you can access RequestBody and RequestHeader as you do in your controller. The Model attribute method is basically to add model attributes which are used across all pages or controller flow. It gets invoked before the controller methods kick in. It provides cleaner way of accessing the RESTful features rather than convoluted way.
#ControllerAdvice(annotations = RestController.class)
public class ControllerAdvisor {
#ModelAttribute
public void addAttributes(HttpServletRequest request, HttpServletResponse response,Model model, #RequestBody String requestString, #RequestHeader(value = "User-Agent") String userAgent) {
// do whatever you want to do on the request body and header.
// with request object you can get the request method and request path etc.
System.out.println("requestString" + requestString);
System.out.println("userAgent" + userAgent);
model.addAttribute("attr1", "value1");
model.addAttribute("attr2", "value2");
}
}
I use #ModelAttribute method to set value from #RequestBody.
#ControllerAdvice
public class CustomRestExceptionHandler extends ResponseEntityExceptionHandler
{
public CustomRestExceptionHandler() {
super();
}
private Object request;
#ModelAttribute
public void setRequest(#RequestBody Object request) {
this.request = request;
}
#Override protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
logger.info(this.request)
}
}

Resources