Spring MVC and multipart handling - spring

I am using Spring MVC 4, and I have a controller with the below mapping/method:
#RequestMapping(value = "/me/bio", method = RequestMethod.POST, consumes = { "multipart/form-data" })
#ResponseBody
public JsonResponse<Boolean> saveProfileBio1(Account account, #RequestPart("file") MultipartFile file, #RequestPart("profile") #Valid ProfileBio profileBio) throws ValidationException, IOException {
...//code here
}
When I submit a multipart form data request it fails with HTTP 400 Bad request with the error " org.springframework.web.multipart.support.MissingS ervletRequestPartException: Required request part 'profile' is not present"
Below is the raw request:
------WebKitFormBoundarynU961NKt3K534rCg
Content-Disposition: form-data; name="profile"
{"profileName":"Zack Smith","profileDescription":"xxx","profileWebLink" :"www.abc","profilePictureUrl":"https://s3.amazonaws.com/xxx-images/default.png","profileTitle":"CTO1"}
------WebKitFormBoundarynU961NKt3K534rCg
Content-Disposition: form-data; name="file"; filename="2013-11-16 21.19.59.jpg"
Content-Type: image/jpeg
As you can see the request clearly has the "profile" part. From my debugging, the issue is that the "profile" request part does not have the "Content-type" set, and DefaultMultipartHttpServletRequest has the below method that requires it to be set and if it returns null the entire request fails with the above error.
#Override
public HttpHeaders getMultipartHeaders(String paramOrFileName) {
String contentType = getMultipartContentType(paramOrFileName);
if (contentType != null) {
HttpHeaders headers = new HttpHeaders();
headers.add(CONTENT_TYPE, contentType);
return headers;
}
else {
return null;
}
}
Trouble is is that I can't seem to find a way to set the content-type on a FormData submit in the browser for each part and seems to be something I can't set, and Spring seems to require it.
Any tips on how to fix this or if this is a bug?
Thanks

I see two options to solve the issue:
On the client: Add the JSON as Blob to FormData, as mentioned here. Background: Blob allows setting the content type (example with angular js):
var formData = new FormData();
formData.append('profile', new Blob([angular.toJson(profile)], {
type: "application/json"}
));
Alternativly on the server (not recommended): overwrite the getMultipartHeaders method of DefaultMultipartHttpServletRequest and configure this in spring. If you are using CommonsMultipartResolver you need to overwrite it as well (due to missing dependency injection point):
new DefaultMultipartHttpServletRequest() {
#Override
public HttpHeaders getMultipartHeaders(String paramOrFileName) {
// your code here
}
}

I was just battling this issue and my solution was to stop using #RequestPart and use #RequestParam instead. If I'm understanding the doc for #RequestPart correctly, it only works out of the box for a few types (such as MultipartFile) but others require an HttpMessageConverter. Also make sure you have a MultipartResolver bean declared. Recommend that it return a CommonsMultipartResolver.

Related

Request with multipart/form-data returns 415 error

I need to receive this request using Spring:
POST /test HTTP/1.1
user-agent: Dart/2.8 (dart:io)
content-type: multipart/form-data; boundary=--dio-boundary-3791459749
accept-encoding: gzip
content-length: 151
host: 192.168.0.107:8443
----dio-boundary-3791459749
content-disposition: form-data; name="MyModel"
{"testString":"hello world"}
----dio-boundary-3791459749--
But unfortunately this Spring endpoint:
#PostMapping(value = "/test", consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public void test(#Valid #RequestPart(value = "MyModel") MyModel myModel) {
String testString = myModel.getTestString();
}
returns 415 error:
Content type 'multipart/form-data;boundary=--dio-boundary-2534440849' not supported
to the client.
And this(same endpoint but with the consumes = MULTIPART_FORM_DATA_VALUE):
#PostMapping(value = "/test", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void test(#Valid #RequestPart(value = "MyModel") MyModel myModel) {
String testString = myModel.getTestString();
}
again returns 415 but, with this message:
Content type 'application/octet-stream' not supported
I already successfully used this endpoint(even without consumes) with this old request:
POST /test HTTP/1.1
Content-Type: multipart/form-data; boundary=62b81b81-05b1-4287-971b-c32ffa990559
Content-Length: 275
Host: 192.168.0.107:8443
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.8.0
--62b81b81-05b1-4287-971b-c32ffa990559
Content-Disposition: form-data; name="MyModel"
Content-Transfer-Encoding: binary
Content-Type: application/json; charset=UTF-8
Content-Length: 35
{"testString":"hello world"}
--62b81b81-05b1-4287-971b-c32ffa990559--
But unfortunately now I need to use the first described request and I can't add additional fields to it.
So, I need to change the Spring endpoint, but how?
You need to have your controller method consume MediaType.MULTIPART_FORM_DATA_VALUE,
#PostMapping(value = "/test", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
......
You also need to add a MappingJackson2HttpMessageConverter support application/octet-stream. In this answer,
I configure it by using WebMvcConfigurer#extendMessageConverters so that I can keep the default configuration of the other converters.(Spring MVC is configured with Spring Boot’s converters).
I create the converter from the ObjectMapper instance used by Spring.
[For more information]
Spring Boot Reference Documentation - Spring MVC Auto-configuration
How do I obtain the Jackson ObjectMapper in use by Spring 4.1?
Why does Spring Boot change the format of a JSON response even when a custom converter which never handles JSON is configured?
#Configuration
public class MyConfigurer implements WebMvcConfigurer {
#Autowired
private ObjectMapper objectMapper;
#Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
ReadOnlyMultipartFormDataEndpointConverter converter = new ReadOnlyMultipartFormDataEndpointConverter(
objectMapper);
List<MediaType> supportedMediaTypes = new ArrayList<>();
supportedMediaTypes.addAll(converter.getSupportedMediaTypes());
supportedMediaTypes.add(MediaType.APPLICATION_OCTET_STREAM);
converter.setSupportedMediaTypes(supportedMediaTypes);
converters.add(converter);
}
}
[NOTE]
Also you can modify the behavior of your converter by extending it.
In this answer, I extends MappingJackson2HttpMessageConverter so that
it reads data only when the mapped controller method consumes just MediaType.MULTIPART_FORM_DATA_VALUE
it doesn't write any response(another converter do that).
public class ReadOnlyMultipartFormDataEndpointConverter extends MappingJackson2HttpMessageConverter {
public ReadOnlyMultipartFormDataEndpointConverter(ObjectMapper objectMapper) {
super(objectMapper);
}
#Override
public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
// When a rest client(e.g. RestTemplate#getForObject) reads a request, 'RequestAttributes' can be null.
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
return false;
}
HandlerMethod handlerMethod = (HandlerMethod) requestAttributes
.getAttribute(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
if (handlerMethod == null) {
return false;
}
RequestMapping requestMapping = handlerMethod.getMethodAnnotation(RequestMapping.class);
if (requestMapping == null) {
return false;
}
// This converter reads data only when the mapped controller method consumes just 'MediaType.MULTIPART_FORM_DATA_VALUE'.
if (requestMapping.consumes().length != 1
|| !MediaType.MULTIPART_FORM_DATA_VALUE.equals(requestMapping.consumes()[0])) {
return false;
}
return super.canRead(type, contextClass, mediaType);
}
// If you want to decide whether this converter can reads data depending on end point classes (i.e. classes with '#RestController'/'#Controller'),
// you have to compare 'contextClass' to the type(s) of your end point class(es).
// Use this 'canRead' method instead.
// #Override
// public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
// return YourEndpointController.class == contextClass && super.canRead(type, contextClass, mediaType);
// }
#Override
protected boolean canWrite(MediaType mediaType) {
// This converter is only be used for requests.
return false;
}
}
The causes of 415 errors
When your controller method consumes MediaType.APPLICATION_OCTET_STREAM_VALUE, it doesn't handle a request with Content-Type: multipart/form-data;. Therefore you get 415.
On the other hand, when your controller method consumes MediaType.MULTIPART_FORM_DATA_VALUE, it can handle a request with Content-Type: multipart/form-data;. However JSON without Content-Type is not handled depending on your configuration.
When you annotate a method argument with #RequestPart annotation,
RequestPartMethodArgumentResolver parses a request.
RequestPartMethodArgumentResolver recognizes content-type as application/octet-stream when it is not specified.
RequestPartMethodArgumentResolver uses a MappingJackson2HttpMessageConverter to parse a reuqest body and get JSON.
By default configuration MappingJackson2HttpMessageConverter supports application/json and application/*+json only.
(As far as I read your question) Your MappingJackson2HttpMessageConverters don't seem to support application/octet-stream.(Therefore you get 415.)
Conclusion
Therefore I think you can successfully handle a request by letting MappingJackson2HttpMessageConverter(an implementation of HttpMessageConverter) to support application/octet-stream like above.
[UPDATE 1]
If you don't need to validate MyModel with #Valid annotation and simply want to convert the JSON body to MyModel, #RequestParam can be useful.
If you choose this solution, you do NOT have to configure MappingJackson2HttpMessageConverter to support application/octet-stream.
You can handle not only JSON data but also file data using this solution.
#PostMapping(value = "/test", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void test(#RequestParam(value = "MyModel") Part part) throws IOException {
// 'part' is an instance of 'javax.servlet.http.Part'.
// According to javadoc of 'javax.servlet.http.Part',
// 'The part may represent either an uploaded file or form data'
try (InputStream is = part.getInputStream()) {
ObjectMapper objectMapper = new ObjectMapper();
MyModel myModel = objectMapper.readValue(part.getInputStream(), MyModel.class);
.....
}
.....
}
See Also
Javadoc of RequestPartMethodArgumentResolver
Javadoc of MappingJackson2HttpMessageConverter
Content type blank is not supported (Related question)
Spring Web MVC - Multipart

Sending file to Spring Boot REST using Axios

I am trying to send a csv file to my java spring boot backend. The code to send my file is below:
var url = 'http://localhost:3001/UploadFile';
var file = this.state.file;
var formData = new FormData();
formData.append("file", file);
axios.post(url, formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
And the code to accept my file from Spring Boot:
#CrossOrigin
#RequestMapping("/UploadFile")
#ResponseBody
public void uploadFile(#RequestParam("file") MultipartFile file) {
}
However, it doesn't seem to work. I keep getting an error saying that the 'Current request is not a multipart request'. Any ideas?
It's not sufficient to specify content-type in frontend you need to do it in controller as well.
You should tell to spring controller what it should consume and also it would be nice to set RequestMethod as POST like this:
#CrossOrigin
#RequestMapping("/UploadFile")
#ResponseBody
public void uploadFile(#RequestParam("file") MultipartFile file, method = RequestMethod.POST, consumes = "multipart/form-data") {
}

How to make a #RestController POST method ignore Content-Type header and only use request body?

I'm using latest Spring Boot (1.2.1) and whatever Spring MVC version comes with it.
I have a controller method with implicit JSON conversions for both incoming and outgoing data:
#RestController
public class LoginController {
#RequestMapping(value = "/login", method = POST, produces = "application/json")
ResponseEntity<LoginResponse> login(#RequestBody LoginRequest loginRequest) {
// ...
}
}
This works fine, but only if request Content-Type is set to application/json. In all other cases, it responds with 415, regardless of the request body:
{
"timestamp": 1423844498998,
"status": 415,
"error": "Unsupported Media Type",
"exception": "org.springframework.web.HttpMediaTypeNotSupportedException",
"message": "Content type 'text/plain;charset=UTF-8' not supported",
"path": "/login/"
}
Thing is, I'd like to make my API more lenient; I want Spring to only use the POST request body and completely ignore Content-Type header. (If request body is not valid JSON or cannot be parsed into LoginRequest instance, Spring already responds with 400 Bad Request which is fine.) Is this possible while continuing to use the implicit JSON conversions (via Jackson)?
I've tried consumes="*", and other variants like consumes = {"text/*", "application/*"} but it has no effect: the API keeps giving 415 if Content-Type is not JSON.
Edit
It looks like this behaviour is caused by MappingJackson2HttpMessageConverter whose documentation says:
By default, this converter supports application/json and
application/*+json. This can be overridden by setting the supportedMediaTypes property.
I'm still missing how exactly do I customise that, for example in a
custom Jackson2ObjectMapperBuilder...
I assume that you are using default MappingJackson2HttpMessageConverter provided by Spring.
If you would like to have the same behavior in all requests, one solution would be to write custom converter which will not look for Content-Type, in a header (instead will parse to JSON alwayse) and then configure Spring to use your custom one. Again this will affect all requests, so might not fit all needs.
public class CustomerJsonHttpMessageConverter extends AbstractHttpMessageConverter<Object> {
private ObjectMapper mapper = new ObjectMapper();
private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
public CustomerJsonHttpMessageConverter() {
super(new MediaType("application", "json", DEFAULT_CHARSET));
}
#Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException,
HttpMessageNotReadableException {
return mapper.readValue(inputMessage.getBody(), clazz);
}
#Override
protected boolean supports(Class<?> clazz) {
return true;
}
#Override
protected void writeInternal(Object value, HttpOutputMessage outputMessage) throws IOException,
HttpMessageNotWritableException {
String json = mapper.writeValueAsString(value);
outputMessage.getBody().write(json.getBytes());
}
}
To have custom media type,
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setSupportedMediaTypes(
Arrays.asList(
new MediaType("text", "plain"),
new MediaType("text", "html")
));
For anyone else who is curious about this;
It is possible to customize the used MappingJackson2HttpMessageConverter by overridding WebMvcConfigurerAdapter.extendMessageConverters to allow for multiple mime types.
However, it does not work as expected because application/x-www-form-urlencoded is hardcoded in ServletServerHttpRequest.getBody to modify the body to be url encoded (even if the post data is JSON) before passing it to MappingJackson2HttpMessageConverter.
If you really needed this to work then I think the only way is to put a Filter that modifies the request content-type header before handling (not to imply this is a good idea, just if the situation arises where this is necessary).
Update: watch out if you use this
(This was probably a stupid idea anyway.)
This has the side effect that server sets response Content-Type to whatever the first value in the request's Accept header is! (E.g. text/plain instead of the correct application/json.)
After noticing that, I got rid of this customisation and settled went with Spring's default behaviour (respond with 415 error if request does not have correct Content-Type).
Original answer:
MappingJackson2HttpMessageConverter javadocs state that:
By default, this converter supports application/json and application/*+json. This can be overridden by setting the supportedMediaTypes property.
...which pointed me towards a pretty simple solution that seems to work. In main Application class:
#Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter converter =
new MappingJackson2HttpMessageConverter(new CustomObjectMapper());
converter.setSupportedMediaTypes(Arrays.asList(MediaType.ALL));
return converter;
}
(CustomObjectMapper is related to other Jackson customisations I have; that contructor parameter is optional.)
This does affect all requests, but so far I don't see a problem with that in my app. If this became a problem, I'd probably just switch the #RequestBody parameter into String, and deserialise it manually.

How to get access to HTTP header information in Spring MVC REST controller?

I am new to web programming in general, especially in Java, so I just learned what a header and body is.
I'm writing RESTful services using Spring MVC. I am able to create simple services with the #RequestMapping in my controllers. I need help understanding how to get HTTP header information from a request that comes to my method in my REST service controller. I would like to parse out the header and get some attributes from it.
Could you explain how I go about getting that information?
When you annotate a parameter with #RequestHeader, the parameter retrieves the header information. So you can just do something like this:
#RequestHeader("Accept")
to get the Accept header.
So from the documentation:
#RequestMapping("/displayHeaderInfo.do")
public void displayHeaderInfo(#RequestHeader("Accept-Encoding") String encoding,
#RequestHeader("Keep-Alive") long keepAlive) {
}
The Accept-Encoding and Keep-Alive header values are provided in the encoding and keepAlive parameters respectively.
And no worries. We are all noobs with something.
You can use the #RequestHeader annotation with HttpHeaders method parameter to gain access to all request headers:
#RequestMapping(value = "/restURL")
public String serveRest(#RequestBody String body, #RequestHeader HttpHeaders headers) {
// Use headers to get the information about all the request headers
long contentLength = headers.getContentLength();
// ...
StreamSource source = new StreamSource(new StringReader(body));
YourObject obj = (YourObject) jaxb2Mashaller.unmarshal(source);
// ...
}
My solution in Header parameters with example is user="test" is:
#RequestMapping(value = "/restURL")
public String serveRest(#RequestBody String body, #RequestHeader HttpHeaders headers){
System.out.println(headers.get("user"));
}
You can use HttpEntity to read both Body and Headers.
#RequestMapping(value = "/restURL")
public String serveRest(HttpEntity<String> httpEntity){
MultiValueMap<String, String> headers =
httpEntity.getHeaders();
Iterator<Map.Entry<String, List<String>>> s =
headers.entrySet().iterator();
while(s.hasNext()) {
Map.Entry<String, List<String>> obj = s.next();
String key = obj.getKey();
List<String> value = obj.getValue();
}
String body = httpEntity.getBody();
}

Spring REST - Can a RestTemplate consume multipart/mixed?

I want to write a REST service which does responed with a zipFile and some json data, everything in one multipart/mixed request.
The server part works fine and i am testing it with the REST Client from firefox. My Server sends a multipart like this
--k-dXaXvCFusLVXUsg-ryiHMmkdttadgcBqi4XH
Content-Disposition: form-data; name="form"
Content-type: application/json
{"projectName":"test","signal":"true"}
--k-dXaXvCFusLVXUsg-ryiHMmkdttadgcBqi4XH
Content-Disposition: form-data; name="file2"; filename="file2.txt"
Content-type: application/octet-stream
Content-Length: 10
hallo=Welt
I know that RestTemplate can send multiparts with the help of a MultiValueMap out of the box.
Now I tried to consume multipart/mixed responses and return a MultiValueMap
#Component
public class RestCommand
extends AbstractLoginRestCommand<Form, MultiValueMap<String, Object>>
{
#Override
protected MultiValueMap<String, Object> executeInternal ( Form form )
{
RestTemplate restTemplate = getRestTemplate();
MyMultiValueMap map = restTemplate.postForObject(getUrl(), form, MyMultiValueMap.class);
return new LinkedMultiValueMap<String, Object>(map);
}
}
class MyMultiValueMap extends LinkedMultiValueMap<String, Object>
{}
MyMultiValueMap exist to prevent type erasure (generics).
This gives
org.springframework.web.client.RestClientException: Could not extract
response: no suitable HttpMessageConverter found for response type
[class org.jlot.client.remote.MyMultiValueMap] and content type
[multipart/form-data;boundary=Rjh-fkdsI9OIyPpYwdFY7lsUIewhRSX8kE19I;charset=UTF-8]
at
org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:107)
at
org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:492)
Javadoc of FormHttpMessageConverter says it can write but not read multipart/form-data.
Why is it like this?
Is there a way to read multipart/form-data with RestTemplate out-of-the-box or do I need to write a HttpMessageConverter?
I had the same issue and I think I achieved what you wanted.
You just have to override the canRead method of the form converter. With your example something like below should work.
FormHttpMessageConverter formConverter = new FormHttpMessageConverter() {
#Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
if (clazz == MyMultiValueMap.class) {
return true;
}
return super.canRead(clazz, mediaType);
}
};
And add this converter to your rest template.
I use this solution at the moment:
#ResponseBody
#PostMapping(value = JlotApiUrls.PUSH, produces = "application/json")
public List<PushResultDTO> push (
#PathVariable String projectName,
#PathVariable String versionName,
#RequestPart("file") MultipartFile multipartFile,
#RequestPart("data") #Valid PushForm pushForm
) throws IOException, BindException
{
...
}
https://github.com/kicktipp/jlot/blob/master/jlot-web/src/main/java/org/jlot/web/api/controller/PushController.java

Resources