嵌入式Jetty文件下载响应丢失Content-Length头问题排查
问题根本原因
你设置的Content-Length头被Jetty自动移除、强制走分块传输的核心原因是你在写入任何响应体内容之前主动调用了response.flushBuffer()。
HTTP协议对Content-Length头有强约束:如果响应带了这个头,实际传输的响应体字节数必须和头里声明的值完全一致,多一字节少一字节都属于协议错误。当你在响应头设置完成、还没写入任何响应体的时候就强制刷缓冲区,会直接触发响应提交,此时Jetty无法确认你后续写入的字节总数是否真的和你声明的Content-Length一致(比如代码中途抛异常、实际写入长度不足),为了避免协议错误,Jetty会主动删除你手动设置的Content-Length头,自动改用Transfer-Encoding: Chunked分块传输模式,这就是浏览器看不到文件总大小、无法显示下载进度的直接原因。
你之前测试设置虚假Content-Length为10000字节时,传输到10000字节就停止,是因为当时没有提前触发flush,Jetty会严格按照你声明的长度截断响应,属于符合HTTP规范的正常行为。
修复方案
- 第一时间删除代码中多余的
response.flushBuffer()调用。你完全不需要手动提前刷出缓冲区,等你把所有响应体内容写入完成后,Servlet容器会自动完成缓冲区刷出、响应头校验、长度匹配的逻辑,此时你设置的Content-Length会被正常保留返回给浏览器。 - 调整字节缓冲区大小,不要使用5MB大小的字节数组做流拷贝,使用8KB~16KB的数组即可,过大的数组只会浪费堆内存,不会带来任何IO性能提升,Jetty的响应流内部已经做了合理的缓冲优化。
- 检查你的嵌入式Jetty配置,如果手动注册了
GzipHandler或者其他响应修改类的过滤器、Handler,需要将你的下载接口路径加入排除规则。这类组件会修改响应体内容(比如压缩),导致最终输出长度和你设置的Content-Length不匹配,同样会触发Jetty自动移除Content-Length头走分块传输。另外mkv这类本身已经经过压缩的媒体文件,开启gzip压缩没有任何收益,反而会额外消耗CPU资源。 - 保留try-with-resources写法处理文件输入流,避免文件句柄泄漏,
ServletOutputStream由容器管理生命周期,不需要手动关闭。 - 所有响应头的设置操作必须放在调用
response.getOutputStream()、写入任何响应体内容之前完成,一旦响应提交(缓冲区刷出、拿到输出流写入第一部分内容),再设置任何响应头都不会生效。 - 删除代码中多余的
EOFException捕获逻辑,FileInputStream读取到文件末尾时会返回-1,不会抛出EOFException,该捕获块永远不会触发。
修正后的参考代码
String filePath = "path/to/file"; // 流拷贝使用8KB缓冲区即可 final int COPY_BUFFER_SIZE = 8192; try { Path file = Paths.get(filePath); long fileLength = Files.size(file); // 所有响应头设置必须在获取输出流之前完成 response.setContentLengthLong(fileLength); response.setHeader("Content-Disposition", "attachment; filename=\"" + fileEntity.fileName + fileEntity.extension + "\""); response.setContentType(Files.probeContentType(file)); try (InputStream in = Files.newInputStream(file); ServletOutputStream out = response.getOutputStream()) { byte[] buffer = new byte[COPY_BUFFER_SIZE]; int bytesRead; while ((bytesRead = in.read(buffer)) != -1) { out.write(buffer, 0, bytesRead); } } } catch (Exception e) { logger.error("File download error", e); }
内容的提问来源于stack exchange,提问作者Alessandro Valentino
相关产品推荐
相关产品推荐

