“getWriter() 已调用”和 Broken pipe 都出现在写响应附近,但不是同一个问题。前者通常是同一响应混用了字符和字节写入入口,后者意味着连接写入失败,需要结合客户端、代理及服务端耗时定位。
一个响应选一个写入者
Servlet 的 getWriter() 用于字符输出,getOutputStream() 用于字节输出。同一个响应不能随意交替使用;框架的模板、消息转换器、过滤器也可能成为先取得写入器的一方。ServletResponse API
旧 Struts 场景中,手工写入后仍让框架进入视图渲染,会产生第二个写入者。原记录提到返回 null 后恢复,但是否停止渲染取决于具体框架契约,不能把 return null 当作任何 Controller 的通用修复。
手工输出时,不再让框架写另一份正文
下面是 Spring MVC、javax.servlet 下的字节响应示例,直接在 void 方法中完成响应:
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class TextDownloadController {
@GetMapping("/download/example")
public void download(HttpServletResponse response) throws IOException {
byte[] content = "演示文本\n".getBytes(StandardCharsets.UTF_8);
response.setContentType("text/plain;charset=UTF-8");
response.setContentLength(content.length);
response.getOutputStream().write(content);
}
}
不要同时加上“再返回一个对象”的路径。JSON 接口更适合交给消息转换器序列化 DTO,不能用替换引号、拼字符串来构造 JSON。
flushBuffer() 会把缓冲内容送出并提交响应,不是“清空响应后再选另一种输出”。提交后状态码和头通常不能重设。reset 与 resetBuffer 也不是一个操作,不能把它们当作重复写入的常规补丁。
Broken pipe 先看连接为什么结束
另一条旧笔记关注客户端取消、代理超时、网络问题或服务端响应过慢。客户端或中间层已经关闭连接时,服务端继续写可能失败;但单凭异常名字不能认定是哪一方的超时配置。
可按时间线核对请求标识、服务端处理时长、代理状态及客户端取消记录。只在符合明确条件时降低已知断连日志级别,不能忽略全部 IOException,更不能为了不报错把所有异常吞掉。
写入失败不等于业务失败
数据库可能已经提交,随后写响应才失败。客户端此时重试,可能重复执行业务,所以修改类接口需要幂等标识和可查询结果。
反过来,前端显示失败也不证明事务一定回滚。把业务提交、响应生成和连接传输分开观察,才不会用第二个异常覆盖第一个真正原因。