Can spring boot controller receive plain/text? - java

I am trying to process a POST request with body of plain text (utf-8) but it seems that spring does not like the plain text nature of the call. Could it be that it is not supported - or otherwise, am I coding it wrong?
#RestController
#RequestMapping(path = "/abc", method = RequestMethod.POST)
public class NlpController {
#PostMapping(path= "/def", consumes = "text/plain; charset: utf-8", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Object> doSomething(#RequestBody String bodyText)
{
...
return ResponseEntity.ok().body(responseObject);
}
}
Respond is:
Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/x-www-form-urlencoded' not supported]
I tested with curl command:
curl -s -X POST -H 'Content-Type: text/plain; charset: utf-8' --data-binary #text.txt localhost:8080/abc/def
The text.txt contains plain text (UTF-8 in Hebrew).

I would like to throw some light on the other part of the question of whether spring supports text/plain?
According to spring docs: what is "consumes" or Consumable Media Types in the #RequestMapping annotation
Definition :
Consumable Media Types
"You can narrow the primary mapping by specifying a list of consumable media types. The request will be matched only if the Content-Type request header matches the specified media type. For example:"
#Controller
#RequestMapping(value = "/pets", method = RequestMethod.POST, consumes="application/json")
public void addPet(#RequestBody Pet pet, Model model) {
// implementation omitted
}
Consumable media type expressions can also be negated as in !text/plain to match to all requests other than those with Content-Type of text/plain.
How spring does the media type matching internally?
Spring Request Driven Design is centred around a servlet called the dispatcher Servlet which uses special beans to process requests one of the bean is RequestMappingHandlerMapping which checks for the media type using
getMatchingCondition method of ConsumesRequestCondition class as shown below.
#Override
public ConsumesRequestCondition getMatchingCondition(ServerWebExchange exchange) {
if (CorsUtils.isPreFlightRequest(exchange.getRequest())) {
return PRE_FLIGHT_MATCH;
}
if (isEmpty()) {
return this;
}
Set<ConsumeMediaTypeExpression> result = new LinkedHashSet<>(expressions);
result.removeIf(expression -> !expression.match(exchange));
return (!result.isEmpty() ? new ConsumesRequestCondition(result) : null);
}
the get matching condition class uses static inner class ConsumesMediaType Expression which actually makes the check
#Override
protected boolean matchMediaType(ServerWebExchange exchange) throws UnsupportedMediaTypeStatusException {
try {
MediaType contentType = exchange.getRequest().getHeaders().getContentType();
contentType = (contentType != null ? contentType : MediaType.APPLICATION_OCTET_STREAM);
return getMediaType().includes(contentType);
}
catch (InvalidMediaTypeException ex) {
throw new UnsupportedMediaTypeStatusException("Can't parse Content-Type [" +
exchange.getRequest().getHeaders().getFirst("Content-Type") +
"]: " + ex.getMessage());
}}
This method returns false once the media type does not match and getMatchingCondition returns null which results in handleNoMatch method of RequestMappingInfoHandlerMapping being called and using PartialMatchHelper class we check for the what type of mismatch it has as shown below and spring throws HttpMediaTypeNotSupportedException error once its see consumes mismatch
if (helper.hasConsumesMismatch()) {
Set<MediaType> mediaTypes = helper.getConsumableMediaTypes();
MediaType contentType = null;
if (StringUtils.hasLength(request.getContentType())) {
try {
contentType = MediaType.parseMediaType(request.getContentType());
}
catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotSupportedException(ex.getMessage());
}
}
throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<>(mediaTypes));
}
Spring supports all media types as per the IANA https://www.iana.org/assignments/media-types/media-types.xhtml the problem lies only with the curl command as quoted by others.

#rumbz
Please refer to the below link it might solve your issue
Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported for #RequestBody MultiValueMap
1 Using annotation
#RequestMapping(value = "/some-path", produces =
org.springframework.http.MediaType.TEXT_PLAIN)
public String plainTextAnnotation() {
return "<response body>";
}
where replace /some-path with whatever you'd like to use.
2 Setting content type in the response entity's HTTP headers:
public String plainTextResponseEntity() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(org.springframework.http.MediaType.TEXT_PLAIN);
return new ResponseEntity("<response body>", httpHeaders, HttpStatus.OK);
}

Per #m-deinum 's comment: The problem is not in the spring framework - but in the fact that curl adds "application/x-www-form-urlencoded" to the request ...
And just to make this question complete:
curl -s -X POST -H "Content-Type:" -H "Content-Type: text/plain; charset: utf-8" --data-binary #file.txt localhost:8080/abc/def

Related

The request body is sent as json even though the content type is set as application/x-www-form-urlencoded

This is related to an existing spring boot question raised by me(Request Body is not properly encoded and hidden when using spring form encoder in Feign Client).
According to this question, we can add either content type in headers or add during request mapping itself as consumes.
So what I did was added content type in headers in the client configuration class
public class EmailClientConfiguration {
#Bean
public RequestInterceptor requestInterceptor(Account<Account> account) {
return template -> {
template.header("Content-Type", "application/x-www-form-urlencoded");
};
}
#Bean
public OkHttpClient client() {
return new OkHttpClient();
}
#Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
#Bean
public Decoder feignDecoder() {
return new JacksonDecoder();
}
#Bean
public Encoder feignFormEncoder () {
return new SpringFormEncoder(new JacksonEncoder());
}
}
and I see in the headers the content type is correctly set as application/x-www-form-urlencoded when the request is sent. But the request body is still sent in json format and also not hidden.
Request Body:
Map<String, String> requestBody = new HashMap<>();
requestBody.put("username", "xyz");
requestBody.put("email", "xyz#gmail.com");
requestBody.put("key", "xxx");
Request Body received in server end:
{"{\n \"key\" : \"xxx\",\n \"email\" : \"xyz#gmail.com\",\n \"username\" : \"xyz\"\n}"
When I add consumes in my request mapping as application/x-www-form-urlencoded
#FeignClient(name = "email", url = "localhost:3000",
configuration = EmailClientConfiguration.class)
public interface EmailClient {
#PostMapping(value = "/email/send", consumes = "application/x-www-form-urlencoded")
ResponseDto sendEmail(#RequestBody Map<String, String> requestBody);
}
it works fine(request body is hidden in server end and also properly encoded). And when I removed the header in the configuration class and adding only consumes works fine without no issues but the vice versa has this problem.
I searched in internet for this and couldn't find any answer.
Feign encodes the request body and parameters before passing the request to any RequestInterceptor (and rightly so). If you do not declare consumes = "application/x-www-form-urlencoded", SprinFormEncoder doesn't know that you're trying to send form data, so it delegates serialization to the inner JacksonEncoder which only does JSON (see for yourself by printing template.body() before setting the header).
Handling such a well-supported header in the interceptor doesn't seem like a good idea, when you already have consumes. If you insist on doing so, you have to provide your own encoder which doesn't rely on the header value and always outputs form-urlencoded data.

Returning ResponseEntity with info

I'm kind of new with Spring. I have made following method:
public ResponseEntity<Borders> updateBorder(#Valid #RequestBody Borders borders) throws URISyntaxException {
log.debug("REST request to update Borders : {}", borders);
Boolean ok = deviceService.updateBorder(borders);
return new ResponseEntity(ok ? HttpStatus.CREATED : HttpStatus.BAD_REQUEST);
}
My method is now returning ResponseEntity with HttpStatus.CREATED or HttpStatus.BAD_REQUEST. When implementing frontEnd to my software I was wondering it would be very handy, if method could also return a String with HttpStatus. Like "Border is updated!" or "Border could not be updated because... pla pla".
What would be best way to return something more specific also to front-end?
you can return something like this , but your method return type should be ResponseEntity < String >, and if you want you pass headers as well.
http://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/ResponseEntity.html
return new ResponseEntity("your message", ok ? HttpStatus.CREATED : HttpStatus.BAD_REQUEST);
ResponseEntity allows to return three types of data:
HTTP Status code
Body
HTTP Header
For each combination of this data you will find a matching constructor. What you are looking for is the body which is an arbitrary object containing the data to be returned. Depending on the Accept header of your request the body will be returned in the requested data format, e.g. application/json. You can even return a simple String as body:
public ResponseEntity<String> updateBorder(#Valid #RequestBody Borders borders) throws URISyntaxException {
log.debug("REST request to update Borders : {}", borders);
Boolean ok = deviceService.updateBorder(borders);
return new ResponseEntity("Border could not be updated", ok ? HttpStatus.CREATED : HttpStatus.BAD_REQUEST);
}

Rest types, JSON or HTML forms with the same URL

I am trying to create a RESTful service and encounter a type conflict within the application. Right now, I deal with this problem by using two different URLs, but this leads to other problems and doesn't feel right.
// Controller to get a JSON
#RequestMapping(value = "/stuff/{stuffId}",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseBody
public stuffDto getStuff(#PathVariable String stuffId) {
return //JSON DTO//
}
// Controller to get an HTML Form
#RequestMapping(value = "/stuff/{stuffId}/form", // <- nasty '/form' here
method = RequestMethod.GET)
public String getStuffForm(#PathVariable String stuffId, ModelMap model) {
// Prepares the model
return "JSP_Form";
}
And on the JavaScript side:
function loadStuffForm(url) {
$.ajax({
type : 'GET',
url : url,
success : function(response) {
showStuffForm(response);
}
});
}
How can I merge both controllers so it will return the right type of data based on what the client accepts? By default it would return a JSON. I want to add 'text/html' somewhere in the ajax query to get the Form instead. Any idea?
You can use Content Negotiation to communicate to the server and tell it what kind of a response you're expecting form it. In your particular scenario, you as a client using an Accept header tell the server to serve a text/html or application/json. In order to implement this, use two different produces with that same URL:
// Controller to get a JSON
#ResponseBody
#RequestMapping(value = "/stuff/{stuffId}", method = GET, produces = "application/json")
public stuffDto getStuff( ... ) { ... }
// Controller to get an HTML Form
#RequestMapping(value = "/stuff/{stuffId}", method = GET, produces = "text/html")
public String getStuffForm( ... ) { ... }
In your requests to /stuff/{id} endpoint, if you send Accept: text/html in headers, the HTML form would return. Likewise, you would get the JSON response by sending Accept: application/json header.
I'm not a JQuery expert but you can check this answer out on how to send an Accept header in $.ajax requests.

Spring Boot optional multipart POST request

I have a service where I want to be able to optionally upload a file (including a file will run a separate function) with a POST request.
A simplified version of what my ReqestMapping looks like is this:
#ApiOperation(value = "Data", nickname = "Create a new data object")
#RequestMapping(value = "/add/{user_id}", produces = "application/json", method = RequestMethod.POST)
public ResponseEntity<Data> addData(#RequestParam("note") String body,
#RequestParam("location") String location,
#RequestParam(value = "file", required = false) List<MultipartFile> file,
#PathVariable String user_id){
if (file != null) {
doSomething(file);
}
doRegularStuff(body, location, user_id);
return new ResponseEntity(HttpStatus.OK);
}
As can be seen, I have the required = false option for my List of multipart files. However, when I attempt to curl the endpoint without any files and while stating that my content type is Content-Type: application/json, I get the error that my request isn't a multipart request.
Fine. So I change to Content-Type: multipart/form-data and without any files, I get the request was rejected because no multipart boundary was found (obviously, since I don't have a file).
This leads me to wonder how I can have a optional multipart parameter in my Spring endpoints? I would like to avoid having to add additional parameters to my request, such as "File Attached: True/False" as that can become cumbersome and unnecessary when the server can just check for existence.
Thanks!
There is no problem in your code, but the problem in client request, because Content-Type should be like below if you want to upload image,
multipart/form-data; boundary="123123"
try to remove the Content-Type header and test, i will put one example for server code and client request
Server code:
#RequestMapping(method = RequestMethod.POST, value = "/users/profile")
public ResponseEntity<?> handleFileUpload(#RequestParam("name") String name,
#RequestParam(name="file", required=false) MultipartFile file) {
log.info(" name : {}", name);
if(file!=null)
{
log.info("image : {}", file.getOriginalFilename());
log.info("image content type : {}", file.getContentType());
}
return new ResponseEntity<String>("Uploaded",HttpStatus.OK);
}
Client Request using Postman
with image
without image
Curl example:
without image, with Content-Type
curl -X POST -H "Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW" -F "name=test" "http://localhost:8080/api/users/profile"
without image, without Content-Type
curl -X POST -F "name=test" "http://localhost:8080/api/users/profile"

Why Spring gives priority to 405 Http Error over 406 Http Error

I have a rest Controller in Spring 3.1 which allows GET and POST methods.
The POST method is restricted to the producing outputs with the mapping
#RequestMapping(value = "projects", method = RequestMethod.POST, produces={"application/json"})
if I POST the next request it returns a 405 Error with "Allow: GET" Header.
POST /resources/projects
Content-Type: application/json
Accept: text/html
I have found that Spring MVC Framework gives priority to 405 Error in RequestMappingInfoHandlerMapping.java in the handleNoMatch method.
protected HandlerMethod handleNoMatch(Set<RequestMappingInfo> requestMappingInfos, String lookupPath, HttpServletRequest request) throws ServletException {
....
....
if (!allowedMethods.isEmpty()) {
throw new HttpRequestMethodNotSupportedException(request.getMethod(), allowedMethods);
}
else if (!consumableMediaTypes.isEmpty()) {
MediaType contentType = null;
if (StringUtils.hasLength(request.getContentType())) {
contentType = MediaType.parseMediaType(request.getContentType());
}
throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<MediaType>(consumableMediaTypes));
}
else if (!producibleMediaTypes.isEmpty()) {
throw new HttpMediaTypeNotAcceptableException(new ArrayList<MediaType>(producibleMediaTypes));
}
But I want my REST API to give priority to the 406 Error, as it is what makes sense, am I right with my priority? How could I achieve that?
I know this issue is resolved in Spring 3.2 but I can't upgrade to 3.2.
I've been suggested to extend RequestMappingInfoHandlerMapping but I don't know how to do it.
I think you've incorrectly configured your application. Either the #RequestMapping isn't as you've shown it, or it's annotating a method in a #Controller that isn't being registered.
With the following being the only mapping registered with my DispatcherServlet
#RequestMapping(value = "projects", method = RequestMethod.POST, produces = { "application/json" })
public String home() {
System.out.println("HomeController: Passing through...");
return "WEB-INF/views/home.jsp";
}
and tested with
CloseableHttpClient httpClient = HttpClientBuilder.create().build();
HttpPost post = new HttpPost("http://localhost:8080/resources/projects");
post.setHeader("Context-type", "application/json");
post.setHeader("Accept", "text/html");
HttpResponse httpResponse = httpClient.execute(post);
System.out.println(httpResponse);
I get
HTTP/1.1 406 Not Acceptable [Server: [...]]
as expected.
You should double check your logs to see if the handler method you want is actually being registered. The logs will show something like
2013-11-30 01:30:33,958 [localhost-startStop-1] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/projects],methods=[POST],params=[],headers=[],consumes=[],produces=[application/json],custom=[]}" onto public java.lang.String xyz.spring.mvc.HomeController.home()
After your comment and the changes you suggested
#RequestMapping(value = "projects", method = RequestMethod.POST, produces = { "application/json" })
public String home() {
System.out.println("HomeController: Passing through...");
return "WEB-INF/views/home.jsp";
}
#RequestMapping(value = "projects", method = RequestMethod.GET)
public String homeGet() {
System.out.println("HomeControllerGet: Passing through...");
return "WEB-INF/views/home.jsp";
}
here are the startup logs
2013-12-02 08:33:57,233 [localhost-startStop-1] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/projects],methods=[POST],params=[],headers=[],consumes=[],produces=[application/json],custom=[]}" onto public java.lang.String xyz.sample.baremvc.HomeController.home()
2013-12-02 08:33:57,234 [localhost-startStop-1] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/projects],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String xyz.sample.baremvc.HomeController.homeGet()
Tomcat still gives me a 406
HTTP/1.1 406 Not Acceptable [[...]]
which makes sense. Remember that the javadoc for the produces attribute states
The format is a single media type or a sequence of media types, with a
request only mapped if the Accept matches one of these media types.
Since your request has an Accept header that doesn't match the media type, the request is not acceptable and a 406 will occur.
Could you subclass RequestMappingInfoHandlerMapping or RequestMappingHandlerMapping?
After dealing with this issue again today, I have a proper answer on how to add a custom Handler Mapping to Spring to solve the problem.
This issue is not happening in Spring 3.2 as #Sotirios Delimanolis pointed out.
If you want to solve it in Spring 3.1 you can:
subclass the HandlerMapping as you can see here:
{
public class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
protected HandlerMethod handleNoMatch(
Set<RequestMappingInfo> requestMappingInfos, String lookupPath,
HttpServletRequest request) throws ServletException {
Set<String> allowedMethods = new HashSet<String>(6);
Set<MediaType> consumableMediaTypes = new HashSet<MediaType>();
Set<MediaType> producibleMediaTypes = new HashSet<MediaType>();
for (RequestMappingInfo info : requestMappingInfos) {
if (info.getPatternsCondition().getMatchingCondition(request) != null) {
if (info.getMethodsCondition().getMatchingCondition(request) == null) {
for (RequestMethod method : info.getMethodsCondition()
.getMethods()) {
allowedMethods.add(method.name());
}
}
if (info.getConsumesCondition().getMatchingCondition(request) == null) {
consumableMediaTypes.addAll(info.getConsumesCondition()
.getConsumableMediaTypes());
}
if (info.getProducesCondition().getMatchingCondition(request) == null) {
producibleMediaTypes.addAll(info.getProducesCondition()
.getProducibleMediaTypes());
}
}
}
if (!producibleMediaTypes.isEmpty()) {
throw new HttpMediaTypeNotAcceptableException(
new ArrayList<MediaType>(producibleMediaTypes));
} else if (!consumableMediaTypes.isEmpty()) {
MediaType contentType = null;
if (StringUtils.hasLength(request.getContentType())) {
contentType = MediaType
.parseMediaType(request.getContentType());
}
throw new HttpMediaTypeNotSupportedException(contentType,
new ArrayList<MediaType>(consumableMediaTypes));
} else if (!allowedMethods.isEmpty()) {
throw new HttpRequestMethodNotSupportedException(
request.getMethod(), allowedMethods);
} else {
return null;
}
}
}
And then you have 2 options:
2.1 Add this bean to your Spring XML Configuration.
<bean name="handlerMapping" class="com.ncr.mobile.rest.handler.CustomRequestMappingHandlerMapping">
<property name="order" value="-1" />
</bean>
2.2 Add this method to you WebConfig. The order works to give the max priority to the class.
#Configuration
public class WebConfig extends WebMvcConfigurationSupport {
#Override
#Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
RequestMappingHandlerMapping handlerMapping = new CustomRequestMappingHandlerMapping();
handlerMapping.setOrder(-1);
handlerMapping.setInterceptors(getInterceptors());
return handlerMapping;
}
}
I hope this can help somebody.

Categories

Resources