You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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();
            }
        }
    }
}

额外优化点

  1. 修正Content-Disposition格式:你原来的代码会生成filename="xxx".docx的错误格式,应该把后缀包含在引号内,比如filename="xxx.docx"。
  2. 使用准确的Content-Type:对于docx文件,用application/vnd.openxmlformats-officedocument.wordprocessingml.document比application/octet-stream更合适,能让浏览器更好识别文件类型。
  3. 大文件处理:如果需要支持大文件,建议用临时文件缓存内容,避免内存溢出,处理完成后记得删除临时文件。

内容的提问来源于stack exchange,提问作者Diamond Dallas

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 04:41:05