【问题标题】:Request with multipart/form-data returns 415 error带有 multipart/form-data 的请求返回 415 错误
【发布时间】:2020-06-07 03:54:41
【问题描述】:

我需要使用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端点:

@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):

@PostMapping(value = "/test", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void test(@Valid @RequestPart(value = "MyModel") MyModel myModel) {
    String testString = myModel.getTestString();
}

再次返回415,但带有此消息:

Content type 'application/octet-stream' not supported

我已经通过这个旧请求成功地使用了这个端点(即使没有consumes):

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

但不幸的是,现在我需要使用第一个描述的请求,并且无法向其中添加其他字段。

所以,我需要更改Spring 端点,但是如何更改?

【问题讨论】:

    标签: spring rest spring-boot http spring-mvc


    【解决方案1】:

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

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

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

    • 我使用WebMvcConfigurer#extendMessageConverters 配置它,这样我就可以保留其他转换器的默认配置。(Spring MVC 配置了 Spring Boot 的转换器)。
    • 我从 Spring 使用的 ObjectMapper 实例创建转换器。

    [详情]
    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 以便

    • 仅当映射的控制器方法仅消耗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;
        }
    }
    



    415 错误的原因

    当您的控制器方法使用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 注释对方法参数进行注释时,

    • RequestPartMethodArgumentResolver 解析请求。
    • RequestPartMethodArgumentResolver 在未指定时将 content-type 识别为 application/octet-stream
    • RequestPartMethodArgumentResolver 使用 MappingJackson2HttpMessageConverter 解析请求主体并获取 JSON。
    • 默认配置MappingJackson2HttpMessageConverter只支持application/json和application/*+json。
    • (据我阅读您的问题)您的MappingJackson2HttpMessageConverters 似乎不支持application/octet-stream。(因此您得到415。)



    结论

    因此,我认为您可以通过让MappingJackson2HttpMessageConverterHttpMessageConverter 的实现)支持application/octet-stream 来成功处理请求,就像上面一样。


    [更新 1]

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

    @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);
    
            .....
        }
        .....
    }
    

    另见

    Javadoc of RequestPartMethodArgumentResolver
    Javadoc of MappingJackson2HttpMessageConverter
    Content type blank is not supported(相关问题)
    Spring Web MVC - Multipart

    【讨论】:

    • 谢谢,它的作品。但是现在我有一个不同的问题:现在我的其他端点(即使没有multipart)有时会以不正确的格式响应。如何指定此 bean 仅应在具有 consumes = MediaType.MULTIPART_FORM_DATA_VALUE 的端点中使用并且仅用于请求,而不用于响应?
    • 我在答案中添加了“更新 1”。这只是一个快速说明,告诉您可能的解决方案。这个解决方案有一些限制(例如你不能用@Valid做验证)所以我还在考虑我们是否有更好的解决方案。
    • 我需要将文件(例如图像)发送到服务器,所以我不确定这个解决方案。我想我需要使用部分。
    • [1/2] 很抱歉耽搁了这么久 - 我以为我可以在客户端解决问题,但不幸的是它没有解决。让我们与转换器讨论解决方案。我仍然面临其他端点响应错误的问题,例如我的端点以毫秒为单位返回日期,但在它之前是在字符串中。这不是问题,但它表明此转换器会影响其他端点。
    • [2/2] 我添加了两个文件:MultipartConfigurer 和 ReadOnlyMultipartFormDataEndpointConverter,并将MultipartConfigurer 中的MappingJackson2HttpMessageConverter 更改为ReadOnlyMultipartFormDataEndpointConverter,但日期仍然存在相同的问题(甚至可能还有一些我还没有注意到的东西)。关于您的问题:我想以相同的方法读取文件和 JSON,例如用于注册 - 用户同时发送帐户信息和头像。
    猜你喜欢
    • 2021-10-11
    • 2019-11-09
    • 2021-08-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-01-14
    • 2022-10-15
    相关资源
    最近更新 更多