一个 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"));
}
排查步骤
上传失败时,先看客户端拿到的状态码和错误信息,再按层排查:
- 413:大概率是 Nginx 或网关。查
nginx.conf的client_max_body_size,改完nginx -s reload。 - 500 且日志有
MaxUploadSizeExceededException:Spring 的限制没放开,或者两个 multipart 配置只改了一个。 - 连接被重置、日志里是
ClientAbortException:可能是max-swallow-size太小,也可能是客户端等不及超时主动断开。 - 请求根本没到后端:用
curl绕过前端直接传,确认是不是网关或 WAF 拦的。 - 落盘失败:检查
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 各管一段。排查时按"客户端报什么错、请求有没有到后端、日志里是什么异常"三步走,基本能定位到具体哪一层。配置改完别忘了两侧都要验证:小文件正常、刚好超限的文件返回友好提示。
评论