在 Chrome 瀏覽器中點擊「列印 PDF」按鈕,後端 Servlet 執行了兩次:
Document.close() 成功完成。org.apache.catalina.connector.ClientAbortException: java.net.SocketException: Software caused connection abort: socket write error
根因:瀏覽器在第一次請求後已關閉連接,第二次請求嘗試向已斷開的連接寫入響應導致。
僅 Chrome/Safari 等 WebKit 內核瀏覽器出現,Firefox/IE 正常。
Chrome 為加速頁面載入,採用 預提取 (Prefetching/Prerendering) 機制:
ClientAbortException。參考資料:Chrome 等瀏覽器的預提取資源機制導致的一個請求發送兩次的問題
application/pdf、application/octet-stream 等非文本響應的介面。Chrome 預載請求會帶有特定 Header:Purpose: prefetch 或 X-Moz: prefetch。在 Servlet/Filter 中攔截:
@WebFilter("/*")
public class PrefetchFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
String purpose = request.getHeader("Purpose");
String xMoz = request.getHeader("X-Moz");
if ("prefetch".equalsIgnoreCase(purpose) || "prefetch".equalsIgnoreCase(xMoz)) {
// 預載請求直接返回 204 No Content,不執行業務邏輯
((HttpServletResponse) res).setStatus(HttpServletResponse.SC_NO_CONTENT);
return;
}
chain.doFilter(req, res);
}
}
預載機制通常僅對 GET 請求生效。將下載/列印操作改為表單 POST 提交或 AJAX POST 可避免預載。但需注意 RESTful 風格與冪等性設計。
在響應中添加:
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
此法無法完全阻止預載請求發送,僅能防止緩存,不建議作為唯一手段。
若業務允許,使下載介面具備冪等性:首次生成 PDF 存入暫存/快取,後續請求直接返回快取內容,避免重複生成與連接異常。
(canceled) 或有 Purpose: prefetch Header。
3. 部署 Filter 後重新測試,確認只剩一筆正式請求。
Chrome 預載機制是為提升用戶體驗,但對非冪等的下載類介面造成干擾。最穩健的做法是在網關或 Filter 層識別 Purpose: prefetch 請求並直接返回 204,避免業務邏輯重複執行與連接異常。