Spring Boot 文件下载:这6个坑让你的下载接口报 500

// ❌ 直接传中文名,浏览器 URL 编码不统一response.setHeader("Content-Disposition","attachment; filename=" + fileName);
// ✅ URLEncoder 统一编码 + UTF-8 声明String encoded = URLEncoder.encode(fileName, StandardCharsets.UTF_8) .replace("+", "+");response.setHeader("Content-Disposition","attachment; filename*=UTF-8''" + encoded);
// ❌ 500M 文件一把 readAllBytes → OOMbyte[] bytes = Files.readAllBytes(file.toPath());response.getOutputStream().write(bytes);
// ✅ 流式边读边写,内存恒定try (InputStream in = new FileInputStream(file); OutputStream out = response.getOutputStream()) {byte[] buf = newbyte[4096];int len;while ((len = in.read(buf)) != -1) { out.write(buf, 0, len); } out.flush();}
// ❌ 没设 Content-Type → 浏览器当文本渲染、下载触发不了response.setHeader("Content-Disposition","attachment; filename=" + fileName);
// ✅ 明确告诉浏览器这是二进制流response.setContentType("application/octet-stream");response.setHeader("Content-Disposition","attachment; filename*=UTF-8''" + encoded);
// ❌ 前端传 fileName=../../etc/passwd,直接拼路径File file = new File(basePath + fileName);
// ✅ 校验 realPath 必须在 basePath 下File file = new File(basePath, fileName);if (!file.getCanonicalPath() .startsWith(new File(basePath).getCanonicalPath())) {thrownew SecurityException("非法路径");}
// ❌ 先设置 header 再写 body 没问题,但忘 flushOutputStream out = response.getOutputStream();Files.copy(file.toPath(), out);// 浏览器拿到截断文件
// ✅ flush 关键:强迫缓冲区写出OutputStream out = response.getOutputStream();Files.copy(file.toPath(), out);out.flush();
# ❌ proxy_buffering on → 大文件全部缓存到 Nginx 磁盘proxy_pass http://backend;
# ✅ 大文件关缓冲 + 限制速率location /download/ { proxy_pass http://backend; proxy_buffering off; proxy_request_buffering off; limit_rate 2m;}
| # | 坑点 | 快速修复 |
|---|---|---|
|
|
|
URLEncoder
filename*=UTF-8'' |
|
|
|
while read write 4KB buf |
|
|
|
application/octet-stream |
|
|
|
getCanonicalPath()
|
|
|
|
out.flush() |
|
|
|
proxy_buffering off |
一句话总结:文件下载核心三条——流式输出别读内存、Content-Disposition 用 UTF-8 编码、路径拼接先校验。
夜雨聆风