Spring #RequestMapping consumes charset? - spring

I'm trying to use #RequestMapping with the consumes-element. Reading the API-document it works on the Content-Type-header of the request. However, using
#RequestMapping(consumes = "application/x-www-form-urlencoded;charset=UTF-8", value = "/test")
public void test() {
:
}
or
#RequestMapping(consumes = "application/x-www-form-urlencoded;charset=ISO-8859-1", value = "/test")
public void test() {
:
}
doesn't make a difference. The header in the request can look like
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
or
Content-Type: application/x-www-form-urlencoded
test() will be called in all four possible constellations.
However, and this is proof to me that Spring sees and tries to use the charset-part, if I specify
#RequestMapping(consumes = "application/x-www-form-urlencoded;charset=UTF-x8", value = "/test")
public void test() {
:
}
I get an exception during startup (!) of the web-app:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#0': Initialization of bean failed;
nested exception is java.nio.charset.UnsupportedCharsetException: UTF-x8
Note that the documentation on the produces-element also doesn't mention the use of charset, but according to Google some use it.
Any clues to what's happening here or what I'm doing wrong?
BTW, this is Spring 3.1.1.RELEASE.

I think you have already answered your question, so this is more of a confirmation, from a code point of view, as to why the charset is not taken into account when resolving mappings.
When digging into Spring code, the culprit seems to be the MediaType#includes(). method
More digging reveals that a RequestMappingInfo is created in association to the RequestMapping annotation of the method. This RequestMappingInfo stores a series of AbstractRequestCondition objects, one of them being the ConsumesRequestCondition which holds the MediaType defined in the consumes part of the annotation (i.e. application/x-www-form-urlencoded;charset=UTF-8).
Later when a request is made, this ConsumesRequestCondition has an inner ConsumeMediaTypeExpression class with a matchMediaType() method that extracts the MediaType of the HttpServletRequest and checks it against it's own MediaType to see if it's included.
If you look at the MediaType#includes() implementation (Lines 426 to 428), it returns true when type (i.e. application) and subtype (i.e. x-www-form-urlencoded) are equal, completely disregarding the parameters Map which in this case
holds the remnant "charset","UTF-8" combination.
Digging into the produces track seems to show similar results, but in this case it's the MediaType#isCompatibleWith() method involved, and again, it reaches only to type and subtype if they are equal.
If you found evidence on Google of the produces working for charset request mapping, I would doubt it (unless they changed core Spring stuff)
As to why it was designed this way, well that's another question :)

Related

5 levels of media type Spring REST

I am trying to apply CQRS principles on my REST API with domain-driven-design principles, using the 5 levels of Media Types, as explained in these articles:
https://www.infoq.com/articles/rest-api-on-cqrs
http://byterot.blogspot.ch/2012/12/5-levels-of-media-type-rest-csds.html
My technical context is Spring REST framework version 3.2.
Basically, i need to be able to map my commands using different "domain-model" media types.
Therefore, i would expect the following mapping to work:
#Controller
#RequestMapping("resources")
public class MyController {
#RequestMapping(value = "{id}", method = RequestMethod.PUT, consumes = "application/json;domain-model=CommandOne")
#ResponseBody
public void commandOne(#PathVariable Long id, #RequestBody CommandOne commandOne) {
LOG.info("Using command {}", commandOne);
}
#RequestMapping(value = "{id}", method = RequestMethod.PUT, consumes = "application/json;domain-model=CommandTwo")
#ResponseBody
public void commandTwo(#PathVariable Long id, #RequestBody CommandTwo commandTwo) {
LOG.info("Using command {}", commandTwo);
}
}
Problem is, I am getting mapping errors when requesting for a PUT:
PUT /resources/123
Content-Type: application/json;domain-model=CommandOne
Error is:
java.lang.IllegalStateException: Ambiguous handler methods mapped for HTTP path ...
Spring doesn't allow me to map the same uri the different domain-model Media Types. Any idea how could I achieve that? Am I missing something?
Many thanks
:o)
That's because the content-type is still the same application/json. Please look at Content-Type syntax
What you are passing as domain-model=CommandOne is just a parameter and Spring doesn't recognize as a difference to call the different methods.
This is described in more detail on answer
Does HTTP content negotiation respect media type parameters
This was submitted as a BUG to the Spring team but they closed with "Work as designed".
Unfortunately Spring can't treat this case currently.

Unit Testing 'Location' header of Spring REST Controller

After creating a resource in Spring REST Controller , I am returning it's location in header as below.
#RequestMapping(..., method = RequestMethod.POST)
public ResponseEntity<Void> createResource(..., UriComponentsBuilder ucb) {
...
URI locationUri = ucb.path("/the/resources/")
.path(someId)
.build()
.toUri();
return ResponseEntity.created(locationUri).build();
}
In Unit Test, I am checking its location as below.
#Test
public void testCreateResource(...) {
...
MockHttpServletRequestBuilder request = post("...")
.content(...)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON);
request.session(sessionMocked);
mvc.perform(request)
.andExpect(status().isCreated())
.andExpect(header().string("Location", "/the/resources" + id);
}
This result cases fails with following message.
java.lang.AssertionError: Response header Location expected:</the/resources/123456> but was:<http://localhost/the/resources/123456>
Seems like I have to provide context prefix http://localhost for Location header in expectation.
Is it safe to hard code context? If so, why?
If not, what is right way to generate it correctly for test case?
I'm guessing because you are using UriComponentsBuilder to build your URI it's setting the host name in your location header. Had you used something like just new URI("/the/resources"), your test would have passed.
In your case, I'd use redirectedUrlPattern to match the redirection URL:
.andExpect(redirectedUrlPattern("http://*/the/resources"))
This will match any hostname, so you don't have to hardcode localhost. Learn more about different patterns that you can use with AntPathMatcherhere.
If you don't need to have a full URI in the Location header on the response (i.e. no requirement, design constraint etc...): Consider switching to use a relative URI ( which is valid from HTTP standards perspective - see [1]: https://www.rfc-editor.org/rfc/rfc7231 ) Relative URIs is a proposed standard that is supported by modern browsers and libraries. This will allow you to test the behavior of the endpoint and make it less fragile in the long run.
If you need to assert the full path, since you are using MockMvc, you can set the uri in the test request to exactly what you want:
#Autowired
private WebApplicationContext webApplicationContext;
#Test
public void testCreateResource() {
MockMvc mvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
mvc.perform(MockMvcRequestBuilders.get(new URI("http://testserver/the/resources")));
This will make the injected builder produce "http://testserver" when build is called. Note of caution, a framework change in the future could cause you headaches if they remove this test behavior.
Facing the same problem, I tried out the solution Suraj Bajaj provided, and it worked fine for me.
I then took a try on asserting the text of the header field "Location" directly and ended up using a containsString().
This simple solution should also be a feasible alternative, as long as server context and the question of relative/absolute path are not of interest:
mvc.perform(request)
.andExpect(status().isCreated())
.andExpect(header().string("Location", containsString("/the/resources"+id)));

Spring-web tries to find resource named with informed path variable

Using spring-web, I am mapping a method to receive a request containing dots "." on the path:
#RequestMapping(value = "download/{id:.+}", method = RequestMethod.GET, produces = "application/xls")
public String download(#PathVariable(value = "id") String id) { ... }
For example, /download/file.xls should be a valid address. But when I try to access that address, Spring returns Could not find acceptable representation as if it was trying to find a resource named file.xls.
Spring shouldn't execute download method rather than try to find a resource named as the path variable?
Obs.: my application is a spring-boot application.
Your #RequestMapping says it produces "application/xls", but your return type is a String and you haven't annotated the return type with #ResponseBody.
If you want to return an Excel spreadsheet, you need to produce that spreadsheet on the server and return it as a byte[] from your request mapping. I'm not sure how or why you'd return a String, unless you're controller is a simple #Controller and you're returning the view name.
Have you tried configuring your RequestMappingHandlerMapping
handler.setUseSuffixPatternMatch( false )
(I was configuring my RequestMappingHandlerMapping anyway, so for me I just needed to add that line - chances are you may be letting Spring Boot autoconfig that class).
See https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.html#setUseRegisteredSuffixPatternMatch-boolean-
Possibly you may need to turn off content negotiation as well - I can't remember exactly what Spring Boot default content negotiation is, but it might be affecting your case.
#Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.favorPathExtension(false)
}
Worth noting that if you are working on a wider/existing application then both these configurations have possible implications more widely, so if that is the case then tread carefully!

Spring MVC: How to treat html suffix like any RequestMapping

I have a legacy system and some url rewrite rule that we want to get rid. One of which is a rule to change is /tools/lookup.html?what=this and change it to /tools/search?what=this and actually returns json and not html !
I'm trying to find a way to have a #Controller to support the legacy format lookup.html, but it fails with HTTP 406 "The resource identified by this request is only capable of generating responses with characteristics not acceptable according to the request "accept" headers.". I'm wondering if anyone as done something similar?
My controller methods looks like:
#RequestMapping(value = "/tools/lookup.html", method = RequestMethod.GET)
public #ResponseBody Result lookup() {
return result;
}
Thanks in advance
Sylvain
Removing the reponsebody annotation will stop the controlle method returning json.
Take a closer look at #RequestMapping, which supports a produces element. E.g.,
#RequestMapping(value = "/tools/search", produces = "application/json")
#ResponseBody
public Result search(...) { ... }
The issue comes from how spring is treating pathvariables. The default behavior will cut off the last dot in a url (.html) to find the request mappings.
This effect happens only for the last pathvariable.
I havent found a property yet to change this globally but one way is to tell your pathvariable mapping to use the regex {pathvariable:.+}.
#Requestmapping("/somepath/{varwithextention:.+}")
public String method(#Pathvariable String varwithextension) {
...
}
Edit: i see that you do not even use pathvars. Probably its still the same effect for the last url part though?
Hi finally found something that should work in my case, note that my app doesn't need to support real html (REST only app), so this shouldn't have to much side effects. In my WebMvcConfigurerAdapter I added the following media type for html.
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.mediaType("html",MediaType.APPLICATION_JSON);
configurer.mediaType("html",MediaType.APPLICATION_XML);
super.configureContentNegotiation(configurer);
}
I now I get my JSON or XML content back in the client. No more 406 error.

Spring MVC 3: Is consumes supposed to disambiguate request mappings?

I have two request mappings in a Spring MVC 3 application, one which takes json and xml, and another that takes application/x-www-form-urlencoded data. Example:
#RequestMapping(value={"/v1/foos"}, method = RequestMethod.POST, consumes={"application/json", "application/xml"})
public FooDTO createFoo(#RequestBody FooDTO requestDTO) throws Exception {
...
}
#RequestMapping(value={"/v1/foos"}, method = RequestMethod.POST, consumes="application/x-www-form-urlencoded")
public FooDTO createFooWithForm(#ModelAttribute FooDTO requestDTO) throws Exception {
...
}
I expected that the different consumes parameter makes each request unique, though I get an java.lang.IllegalStateException: Ambiguous handler methods mapped....
Should consumes and produces makes requests unique? Any ideas?
Edit 1: To add weight to this, if you set the content-type in the header rather than using consumes, this actually works and makes them unique: headers="content-type=application/x-www-form-urlencoded. Perhaps there is a bug with consumes?
Edit 2: We're using Spring 3.1.1.RELEASE.
This has been resolved by Marten Deinum on the Spring Forum (here):
You should change both the HandlerMapping as well as the
HandlerAdapter (use the RequestMappingHandlerAdapter).
In theory it should work if it doesn't feel free to register an issue.
The solution to this problem was to use the correct HandlerMapping and HandlerAdapter in my servlet configuration:
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"/>
Thanks Marten.

Resources