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,
WebMvcConfigurer#extendMessageConverters
so that I can keep the default configuration of the other converters.(Spring MVC is configured with Spring Boot’s converters).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
MediaType.MULTIPART_FORM_DATA_VALUE
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.MappingJackson2HttpMessageConverter
supports application/json and application/*+json only.MappingJackson2HttpMessageConverter
s 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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With