ElIauk

Spring Boot 文件上传大小限制踩坑

一个 413 引出的连环问题 文件上传这个功能,本地测试时好好的,一上测试环境就出问题:小文件能传,超过 1MB 就报错;前端看到的是"网络错误",后端日志里却什么都没有;运维说 Nginx 返回了 413,改完 Nginx 又变成 500;等后端把限制调大,上传大文件时连接直接被重置。 这类问题不

评论
Spring Boot 文件上传大小限制踩坑

一个 413 引出的连环问题

文件上传这个功能,本地测试时好好的,一上测试环境就出问题:小文件能传,超过 1MB 就报错;前端看到的是"网络错误",后端日志里却什么都没有;运维说 Nginx 返回了 413,改完 Nginx 又变成 500;等后端把限制调大,上传大文件时连接直接被重置。

这类问题不是某一个配置的锅,而是上传链路上每一层都有自己的大小限制。只改其中一层,问题就会从这一层转移到下一层。把这几个限制点搞清楚,排查会快很多。

限制藏在哪几层

层 配置项 默认值
Spring Boot spring.servlet.multipart.max-file-size 1MB
Spring Boot spring.servlet.multipart.max-request-size 10MB
Tomcat server.tomcat.max-swallow-size 2MB
Nginx client_max_body_size 1m
API 网关 各组件不同 需单独确认

max-file-size 限制单个文件,max-request-size 限制整个请求(多文件上传、表单字段都算)。这两个值要一起改,只改前者,多文件场景仍然会被后者挡住。

max-swallow-size 稍微特殊:请求体超限时,Tomcat 默认只"吞掉"2MB 剩余数据就断开连接。客户端看到的是连接重置,而不是一个友好的错误响应。把它设为 -1 可以吞完整个请求体再返回错误,代价是浪费带宽和时间,一般只在上传接口上按需调整。

配置与代码

先改 Spring Boot 的配置:

spring:
  servlet:
    multipart:
      enabled: true
      max-file-size: 20MB
      max-request-size: 25MB
      location: /data/tmp/upload
server:
  tomcat:
    max-swallow-size: -1

location 建议显式指定到数据盘,不要依赖系统临时目录。很多容器镜像的 /tmp 空间很小,传几个大文件就满了。

Controller 侧要做三件事:判空、校验类型、用随机文件名落盘:

@PostMapping("/upload")
public Result<String> upload(@RequestParam("file") MultipartFile file) throws IOException {
    if (file.isEmpty()) {
        throw new BusinessException(40002, "文件不能为空");
    }
    String ext = StringUtils.getFilenameExtension(file.getOriginalFilename());
    if (ext == null || !Set.of("jpg", "png", "pdf").contains(ext.toLowerCase())) {
        throw new BusinessException(40003, "不支持的文件类型");
    }
    String fileName = UUID.randomUUID().toString().replace("-", "") + "." + ext;
    Path target = Paths.get("/data/upload", fileName);
    Files.createDirectories(target.getParent());
    file.transferTo(target);
    return Result.ok(fileName);
}

超限异常单独处理,给前端一个明确提示:

@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity<Result<Void>> handleMaxUpload(MaxUploadSizeExceededException e) {
    return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE)
            .body(Result.fail(40004, "文件过大,单个文件不能超过 20MB"));
}

排查步骤

上传失败时,先看客户端拿到的状态码和错误信息,再按层排查:

  1. 413:大概率是 Nginx 或网关。查 nginx.conf 的 client_max_body_size,改完 nginx -s reload。
  2. 500 且日志有 MaxUploadSizeExceededException:Spring 的限制没放开,或者两个 multipart 配置只改了一个。
  3. 连接被重置、日志里是 ClientAbortException:可能是 max-swallow-size 太小,也可能是客户端等不及超时主动断开。
  4. 请求根本没到后端:用 curl 绕过前端直接传,确认是不是网关或 WAF 拦的。
  5. 落盘失败:检查 location 目录的磁盘空间和写权限,容器里还要确认挂载卷。
curl -i -F "file=@./demo.pdf" http://127.0.0.1:8080/api/upload

用这条命令逐层测试,能快速区分是应用的问题还是代理的问题。

常见坑

1. 只改 max-file-size

多文件上传时,max-request-size 才是总闸。两个值都改,并且 max-request-size 要略大于文件大小之和。

2. 数值没写单位

max-file-size: 20 表示 20 字节,不是 20MB。要写 20MB。这个错误很隐蔽,因为配置能启动,只是所有上传都失败。

3. 忘了 Nginx 的默认 1MB

Spring 调大了,Nginx 还是默认的 1m,超过 1MB 直接 413,请求到不了 Tomcat。上传接口所在的 location 或 server 块里都要确认。

4. 临时目录空间不足

MultipartFile 超过一定大小会写到磁盘临时文件,目录满了会抛 IOException。容器部署时尤其要注意 /tmp 是不是 tmpfs、容量有多大。

5. MultipartFile 为 null

表单里 enctype 写成了默认的 application/x-www-form-urlencoded,或者 @RequestParam 的名字和前端字段名不一致,都会拿到 null。先确认请求头是 multipart/form-data。

6. transferTo 用了相对路径

相对路径的基准是 Tomcat 的临时目录,不是你以为的项目目录,重启后文件就找不到了。统一用绝对路径,并在配置里固定上传根目录。

7. 把整个文件读进内存

file.getBytes() 对几 MB 的文件还行,上百 MB 就有 OOM 风险。大文件用 transferTo 或流式拷贝,不要转成字节数组再处理。

8. 文件名直接拼路径

用户上传的文件名可能包含 ../,直接拼接会造成路径穿越。用 UUID 重命名,原始文件名只存数据库用于展示。

小结

上传大小限制是一条链:客户端校验、Nginx、网关、Tomcat、Spring 各管一段。排查时按"客户端报什么错、请求有没有到后端、日志里是什么异常"三步走,基本能定位到具体哪一层。配置改完别忘了两侧都要验证:小文件正常、刚好超限的文件返回友好提示。

参考

评论

推荐阅读

订阅邮件

订阅更新,及时收到最新文章。

作者