首页 > 解决方案 > 在 Spring Boot 2.1 MultipartFile 上出现近 50% http 400 错误请求错误 - 启用 SSL(https)时文件上传

问题描述

我有一个在公共域上使用 Spring Boot 2.1 构建的 API 服务器,它也提供 API 和文件上传。

最近几天,我们想升级这个 Spring Boot 服务器以使用 SSL (https)。在我们在 Spring Boot 中设置 SSL 设置之前。文件上传的API很好用(100%上传成功)。

在我们在 Spring Boot 中设置 SSL 设置之后。文件上传的 API 有效,但只有 50% 上传成功,其他 50% 收到 http 400 错误请求。(我们确定问题与前端web无关,因为我们使用Spring Boot捆绑的Swagger进行测试可以得到相同的结果)

我们查找 Spring Boot 的服务器日志。当 http 400 错误请求发生时,没有任何关于 http 400 错误请求的日志。我们研究了很多天并在互联网上进行了调查,但仍然无法解决这个问题。请给予帮助。

我们已经尝试禁用 csrf(在属性文件中或通过配置类)和许多其他在互联网上提供但仍然无法正常工作的解决方案。

环境:Spring Boot 2.1.13(这是Spring Boot 2.1的最新版本)

属性文件中的设置:(仅在属性文件中添加了SSL设置部分,并且SSL(https)已成功打开)

# SSL setup
server.ssl.key-store=classpath:keystore.p12
server.ssl.key-store-password=abcdef
server.ssl.key-store-type=PKCS12
server.ssl.key-alias=tomcat
server.ssl.enabled-protocols=TLSv1.1,TLSv1.2
server.http2.enabled=true
security.basic.enabled=false
security.enable-csrf=false

## MULTIPART (MultipartProperties)
# Enable multipart uploads
spring.servlet.multipart.enabled = true
# Threshold after which files are written to disk.
spring.servlet.multipart.file-size-threshold=2KB
# Max file size.
spring.servlet.multipart.max-file-size=100MB
# Max Request Size
spring.servlet.multipart.max-request-size=115MB

我的文件上传控制器:

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.SwaggerDefinition;
import io.swagger.annotations.Tag;
import java.util.Collections;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@Slf4j
@RestController
@RequestMapping(value = "/v1/fileupload")
@Api(tags = {"fileupload api"}, value = "fileupload")
@SwaggerDefinition(tags = {
    @Tag(name = "fileupload api", description = "apis for file upload")
})
public class FileUploadController {

    @Autowired
    private FileUploadService fileUploadService;

    private ApiUtilHelper helper = new ApiUtilHelper();

    @ApiOperation(value = "upload single data import file")
    @RequestMapping(
        value = "/dataimport",
        method = RequestMethod.POST,,
        consumes = { MediaType.MULTIPART_FORM_DATA_VALUE },
        produces = { MediaType.APPLICATION_JSON_UTF8_VALUE }
    )
    public ResponseEntity<?> uploadSingleFileForDataImport(@RequestParam("file") MultipartFile file) throws FileStorageException {
        log.info("Enter into uploadSingleFileForDataImport");
        FileUploadResponse fileUploadResponse = fileUploadService.storeFile(file, "dataImport");
        Map<String, Object> additionals = Collections.singletonMap("filupload", fileUploadResponse);
        BasicResponse br = helper.createSuccessBaseResponse(ApiSuccessCode.CreateSuccess, additionals);
        return new ResponseEntity<BasicResponse>(br, ApiSuccessCode.CreateSuccess.getHttpStatus());
    }

大摇大摆的测试结果:

Request URL: https://example.com:8443/v1/fileupload/dataimport
Request Method: POST
Status Code: 400 
Remote Address: 111.222.111.222:8443
Referrer Policy: no-referrer-when-downgrade

**Response http header from Spring Boot**
Connection: close
Content-Length: 0
Date: Mon, 20 Apr 2020 13:13:02 GMT

**Request http header from Swagger**
accept: application/json;charset=UTF-8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7,zh-CN;q=0.6,ja;q=0.5
Connection: keep-alive
Content-Length: 484098
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryiXmuHnaNthhXowmb
Cookie: _ga=GA1.2.1299976434.1580821082; JSESSIONID=2C157019D6560405CC75A5F5083DE0AE
Host: example.com:8443
Origin: https://example.com:8443
Referer: https://example.com:8443/swagger-ui.html
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.113 Safari/537.36

2020.04.20 13:44(UTC 时间)补充信息如下:谢谢@nbalodi,当我设置 logging.level.org.springframework.web=DEBUG。我现在得到了错误日志。附上的日志如下:

HttpEntityMethodProcessor : No match for [application/json;charset=UTF-8], supported: []
ExceptionHandlerExceptionResolver : Resolved [org.springframework.web.multipart.support.MissingServletRequestPartException: Required request part 'file' is not present]
DispatcherServlet : Completed 400 BAD_REQUEST

奇怪的是,这种错误情况仅在我们使用如上所述的 ssl 设置时才会发生。

标签: javaspringspring-bootfile-uploadmultipartform-data

解决方案


谢谢大家的回复。我认为这是 SpringBoot 的错误或其在 SpringBoot v2.1.x 版本中嵌入的 Tomcat 错误。SpringBoot v2.3.0 正式版发布时。我使用相同的代码升级到 v2.3.0 现在一切正常。我在文件上传中使用批量测试或称为压力测试。现在已经100%成功了。

更新: 根本原因是 Tomcat 组件在 Spring 框架下不稳定 - 启用了 https 和/或 http2 或 Spring 安全性(如 OAuth2)下的 Multipart。

在 Spring Boot 2.3.0 - 2.3.2 大部分情况下,文件上传失败率在 4 ~ 14 ~ 50%。Spring Boot 2.3.5 到 2.4.0 版本后,文件上传失败率接近 50%。关键的不同是这些 Spring Boot 版本之间的 Tomcat 版本和 Spring Security 版本不同。

另一个问题是Tomcat支持的http2协议不稳定。如果启用 http2(例如在属性文件中设置 server.http2.enabled=true )并使用 Multipart 或 HttpServletRequest 进行文件上传,则失败率上升到接近 90% 并获得连接。

结论: 对于任何面临文件上传不稳定问题并获得 ERR_CONNECTION_CLOSED 并找到错误日志的人

org.apache.catalina.connector.ClientAbortException:org.apache.coyote.CloseNowException:连接 [{0}],流 [{1}],此流不可写

原因:org.apache.coyote.CloseNowException:连接 [{0}],流 [{1}],此流不可写

原因:org.apache.coyote.http2.StreamException:连接 [{0}],流 [{1}],此流不可写

您可以尝试升级到 Spring Boot 2.4.0 并通过在属性文件中设置 server.http2.enabled=false 来禁用 Spring Boot 中对 Tomcat 的 http2 支持

您可能会发现稳定性甚至提高到 100% 的成功率。(我尝试并实施给我所有的客户)

如果禁用 http2 支持对您不适用。您可以尝试降级到 Spring Boot 2.3.0 - 2.3.2 版本或通过 HttpServletRequest 实现文件上传(记得先禁用 Spring Boot 中的 Multipart)

你可以在这里那里找到相关的开发文档


推荐阅读