具有多部分/表单数据的请求返回415错误 [英] Request with multipart/form-data returns 415 error

查看:575
本文介绍了具有多部分/表单数据的请求返回415错误的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我需要使用Spring接收此请求:

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--

但是不幸的是,这个Spring端点:

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();
}

返回415错误:

Content type 'multipart/form-data;boundary=--dio-boundary-2534440849' not supported

给客户.

这(相同的端点,但带有consumes = MULTIPART_FORM_DATA_VALUE):

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();
}

再次

返回415,但显示以下消息:

again returns 415 but, with this message:

Content type 'application/octet-stream' not supported

我已经将此旧请求成功使用了此端点(即使没有consumes):

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.

因此,我需要更改Spring端点,但是如何?

So, I need to change the Spring endpoint, but how?

推荐答案

您需要让控制器方法使用MediaType.MULTIPART_FORM_DATA_VALUE

You need to have your controller method consume MediaType.MULTIPART_FORM_DATA_VALUE,

@PostMapping(value = "/test", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
......

您还需要添加MappingJackson2HttpMessageConverter支持application/octet-stream.在这个答案中,

You also need to add a MappingJackson2HttpMessageConverter support application/octet-stream. In this answer,

  • 我使用WebMvcConfigurer#extendMessageConverters对其进行配置,以便保留其他转换器的默认配置.(Spring MVC是使用Spring Boot的转换器配置的.)
  • 我从Spring使用的ObjectMapper实例创建转换器.
  • 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.

[更多信息]
春季引导参考文档-Spring MVC自动配置
如何获取杰克逊Spring 4.1正在使用ObjectMapper?
为什么即使配置了一个从不处理JSON的自定义转换器,Spring Boot为何仍会更改JSON响应的格式?

[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);
    }

}

[注意]
您还可以通过扩展转换器来修改其行为.
在这个答案中,我将MappingJackson2HttpMessageConverter扩展为

[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
  • 时才读取数据
  • 它没有写任何响应(另一个转换器这样做).
  • 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;
    }
}



415错误的原因



The causes of 415 errors

当控制器方法使用MediaType.APPLICATION_OCTET_STREAM_VALUE时,它不会使用Content-Type: multipart/form-data;处理请求.因此,您得到415.

另一方面,当您的控制器方法使用MediaType.MULTIPART_FORM_DATA_VALUE时,它可以使用Content-Type: multipart/form-data;处理请求.但是,根据您的配置,不处理不带Content-Type的JSON.
当您使用@RequestPart批注对方法参数进行批注时,

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解析请求.
  • RequestPartMethodArgumentResolver在未指定内容类型时将其识别为application/octet-stream.
  • RequestPartMethodArgumentResolver使用MappingJackson2HttpMessageConverter解析reuqest正文并获取JSON.
  • 默认配置MappingJackson2HttpMessageConverter仅支持application/json和application/* + json.
  • (据我所读的问题),您的MappingJackson2HttpMessageConverter似乎不支持application/octet-stream.(因此您会获得415.)
  • 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

因此,我认为您可以通过让MappingJackson2HttpMessageConverter(HttpMessageConverter的实现)支持上述的application/octet-stream来成功处理请求.


[UPDATE 1]

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]

如果您不需要使用@Valid批注验证MyModel,而只想将JSON主体转换为MyModel,则@RequestParam会很有用.
如果选择此解决方案,则不必不需要即可配置MappingJackson2HttpMessageConverter以支持application/octet-stream.
使用此解决方案,您不仅可以处理JSON数据,而且还可以处理文件数据.

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);

        .....
    }
    .....
}

另请参见

RequestPartMethodArgumentResolver的Javadoc
MappingJackson2HttpMessageConverter的Javadoc
内容类型为空白(相关问题)
Spring Web MVC-Multipart

Javadoc of RequestPartMethodArgumentResolver
Javadoc of MappingJackson2HttpMessageConverter
Content type blank is not supported (Related question)
Spring Web MVC - Multipart

这篇关于具有多部分/表单数据的请求返回415错误的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆