Java Web应用REST获取文件触发浏览器下载报错排查
问题根源与解决方案
这个错误的核心原因是你提前关闭了HttpClient的响应资源,导致返回的输入流在Servlet使用时已经被关闭。让我具体拆解一下:
在你的getDocumentRequest方法里,你用try-with-resources创建了CloseableHttpClient,并且在finally块里关闭了CloseableHttpResponse。但HttpClient的设计是:当你关闭CloseableHttpResponse时,会自动关闭关联的实体输入流;同时try-with-resources块结束时,CloseableHttpClient也会被关闭——这就导致当你的Servlet代码尝试读取InputStream in时,这个流已经被彻底关闭了,自然读不到任何内容,触发了"Premature end of Content-Length delimited message body"的错误。
修复方案
方案一:缓存流内容到内存(适合小文件场景)
把外部REST返回的内容先读取到内存字节数组中,再返回一个独立的输入流,这样就不再依赖原HttpClient的资源生命周期:
public InputStream getDocumentRequest(String docID) { String restREQ = "MY_REST_URL" + docID + "/content"; String basic_auth = new String(Base64.encodeBase64(("SERVICE_LOGIN" + ":" + "SERVICE_PASS").getBytes())); try( CloseableHttpClient httpclient = HttpClients.custom().build(); ) { HttpGet httpget = new HttpGet(restREQ); httpget.addHeader("Authorization", "Basic " + basic_auth); try (CloseableHttpResponse response = httpclient.execute(httpget)) { HttpEntity entity = response.getEntity(); if (entity != null) { // 将实体内容读取到字节数组,再包装成独立的输入流 byte[] contentBytes = EntityUtils.toByteArray(entity); EntityUtils.consume(entity); // 确保HttpClient的实体资源被正确释放 return new ByteArrayInputStream(contentBytes); } } } catch(Exception e) { e.printStackTrace(); } return null; }
方案二:Servlet端统一管理资源生命周期(适合大文件场景)
如果文件过大,内存缓存可能导致OOM,可以重构代码,让Servlet端负责HttpClient和响应的生命周期管理:
// 修改RestHelper,返回CloseableHttpResponse而不是InputStream public CloseableHttpResponse getDocumentResponse(String docID) throws IOException { String restREQ = "MY_REST_URL" + docID + "/content"; String basic_auth = new String(Base64.encodeBase64(("SERVICE_LOGIN" + ":" + "SERVICE_PASS").getBytes())); CloseableHttpClient httpclient = HttpClients.custom().build(); HttpGet httpget = new HttpGet(restREQ); httpget.addHeader("Authorization", "Basic " + basic_auth); return httpclient.execute(httpget); } // Servlet代码 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { RestHelper helper = new RestHelper(); String docName = request.getParameter("documentName"); String docID = request.getParameter("documentID"); CloseableHttpClient httpclient = null; try (CloseableHttpResponse restResponse = helper.getDocumentResponse(docID); InputStream in = restResponse.getEntity().getContent(); ServletOutputStream out = response.getOutputStream()) { if (in == null) { response.sendError(HttpServletResponse.SC_NOT_FOUND, "Document not found"); return; } // 修正Content-Type和Content-Disposition的格式 response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document"); response.setHeader("Content-Disposition", "attachment;filename=\"" + docName + ".docx\""); IOUtils.copy(in, out); out.flush(); } catch (Exception e) { e.printStackTrace(); response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } finally { // 最后关闭HttpClient if (httpclient != null) { try { httpclient.close(); } catch (IOException e) { e.printStackTrace(); } } } }
额外优化点
- 修正Content-Disposition格式:你原来的代码会生成
filename="xxx".docx的错误格式,应该把后缀包含在引号内,比如filename="xxx.docx"。 - 使用准确的Content-Type:对于docx文件,用
application/vnd.openxmlformats-officedocument.wordprocessingml.document比application/octet-stream更合适,能让浏览器更好识别文件类型。 - 大文件处理:如果需要支持大文件,建议用临时文件缓存内容,避免内存溢出,处理完成后记得删除临时文件。
内容的提问来源于stack exchange,提问作者Diamond Dallas
相关产品推荐
相关产品推荐

