
本文详解 spring boot 中通过 responseentity<inputstreamresource> 返回 pdf 文件时,为何浏览器不触发下载、仅显示乱码,并提供包含正确响应头、跨域配置及前端调用建议的完整实践方案。
本文详解 spring boot 中通过 responseentity<inputstreamresource> 返回 pdf 文件时,为何浏览器不触发下载、仅显示乱码,并提供包含正确响应头、跨域配置及前端调用建议的完整实践方案。
在 Spring Boot 应用中,后端成功生成 PDF 流(如 ByteArrayInputStream)并返回 ResponseEntity<InputStreamResource> 是常见做法。但许多开发者遇到一个典型问题:Postman 能正常预览 PDF,而浏览器点击下载按钮后却只显示原始 PDF 二进制内容(如 %PDF-1.4 开头的乱码),并未触发文件下载。根本原因在于:HTTP 响应缺少明确且正确的 Content-Type 响应头,导致浏览器无法识别资源类型并启用附件下载行为。
✅ 正确响应头是关键
虽然 @PostMapping(..., produces = "application/pdf") 声明了控制器支持的媒体类型(用于 Spring MVC 的内容协商和请求映射匹配),但它不会自动设置实际 HTTP 响应的 Content-Type 头。当手动构造 ResponseEntity 并写入原始字节流时,必须显式添加:
headers.add("Content-Type", "application/pdf");⚠️ 注意:使用 "Content-type"(小写 t)在部分环境可能被忽略,推荐使用标准常量 HttpHeaders.CONTENT_TYPE,确保大小写与规范一致。
以下是修复后的完整后端代码示例:
@PostMapping(value = "/download/generateReport/{id}", produces = "application/pdf")
public ResponseEntity<InputStreamResource> generateFavoriteReport(@PathVariable String id) throws Exception {
ReportRequest reportRequest = this.reportService.findByRequestWithId(id);
ByteArrayInputStream in = this.reportService.generateFavoriteReport(reportRequest);
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=FavoriteReport.pdf");
headers.add(HttpHeaders.CONTENT_TYPE, "application/pdf"); // ✅ 强制声明 MIME 类型
headers.add("Access-Control-Expose-Headers", "Content-Disposition"); // 仅暴露必要头,更安全
headers.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate");
headers.add(HttpHeaders.PRAGMA, "no-cache");
headers.add(HttpHeaders.EXPIRES, "0");
return ResponseEntity.ok()
.headers(headers)
.body(new InputStreamResource(in));
}? 补充说明与最佳实践
- produces 的作用:仅影响 Spring 的 HandlerMapping 和 ContentNegotiationManager,用于决定是否将该请求路由到此方法(例如区分 /report.pdf 和 /report.json)。它不参与响应体的实际序列化或头设置。
- Content-Disposition: attachment:这是触发浏览器下载的核心头。若省略或拼写错误(如 content-disposition 小写),多数现代浏览器将尝试内联渲染(尤其对 PDF),而非下载。
-
跨域注意事项(CORS):若前端与后端域名不同,除 Access-Control-Expose-Headers 外,还需确保全局或控制器级 CORS 配置允许 Content-Disposition 头被前端 JavaScript 访问(用于调试或自定义下载逻辑):
@Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList("https://your-frontend.com")); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "OPTIONS")); configuration.setExposedHeaders(Arrays.asList("Content-Disposition")); // 关键! UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } -
前端调用方式建议:
❌ 错误:直接用 fetch() 或 axios.get() 获取 PDF 流后尝试 window.open(URL.createObjectURL(blob)) —— 易受 CORS 和 Blob URL 生命周期限制。
✅ 推荐:使用 <a> 标签 download 属性发起导航(无需 JS 解析响应):<a href="/api/download/generateReport/123" download="FavoriteReport.pdf" class="btn btn-primary">下载报告</a>
或在 JS 中动态触发:
function downloadReport(id) { const link = document.createElement('a'); link.href = `/api/download/generateReport/${id}`; link.download = 'FavoriteReport.pdf'; document.body.appendChild(link); link.click(); document.body.removeChild(link); }
? 总结
浏览器不下载 PDF 的本质是响应头缺失或不规范。只需三步即可彻底解决:
- 强制设置 Content-Type: application/pdf(使用 HttpHeaders.CONTENT_TYPE 常量);
- 正确声明 Content-Disposition: attachment; filename=xxx.pdf;
- 确保跨域场景下 Content-Disposition 被列入 Access-Control-Expose-Headers。
完成上述配置后,无论是点击链接还是调用接口,浏览器均会弹出保存对话框,PDF 下载流程即告稳定可靠。


















