花拾录
← 返回知识库

加了一个日志过滤器,所有 POST 接口突然报"必需请求体缺失"

后端开发导入2026/09/220 阅读0 评论

你给后端加了一个自定义日志过滤器,想把每个请求的出入参都记下来方便排查。加完之后,所有 POST 接口齐刷刷报错,说请求体是空的。

现象

加了一个自定义日志过滤器之后,所有 POST 接口突然报:

Required request body is missing

GET 接口正常,但凡是需要读 body 的 POST / PUT 全部失败。日志里能看到请求确实进来了,参数也确实传了,但框架读的时候说没有。

最迷惑的地方是:过滤器的代码看起来完全正常——它只是读了一下 body 记日志而已,为什么会让别人读不到?

这个"为什么只影响 POST"的特征本身就是一条强线索:GET 请求通常没有 body,过滤器读不读都无所谓;只有带 body 的方法才会暴露问题。看到"只有 POST / PUT 挂、GET 正常",就该往"请求体被谁消费了"这个方向想。

根因

这是请求输入流只能读一次的经典问题。

HTTP 请求体在 Servlet 之类的模型里是一个输入流。流有一个特性——读到底就没了,不能倒回去重读,除非底层支持 mark / reset。

自定义日志过滤器的典型写法是用一个缓存包装器(wrapper)把 HttpServletRequest 包起来,先读一遍 body 记到日志里,再把包装后的请求传给后续的处理链,期望框架从缓存里读。

但如果包装器实现不正确——比如读了流却没有把缓存正确暴露给框架、或者 getInputStream 返回的还是原始的、已经读到底的流——那么框架拿到的就是一个空流,于是判定"请求体缺失"。

一句话概括:过滤器消费了请求体,却没有负责把它"还原"回去,后续的消费者自然读不到。

这里还牵涉到过滤器的执行顺序:过滤器在进入业务代码之前运行,是链条里第一个碰到请求体的组件。它读完之后把请求(或者被包装过的请求,或者干脆是原始请求)往后传——传到框架的参数解析器时,流的位置已经到底了。

解决

方案一(最直接):删掉这个自定义过滤器,改用框架自带的日志配置。

绝大多数后端框架都内置了请求日志能力:

# Spring Boot 为例
logging.level.org.springframework.web=DEBUG

这样做的好处是不用自己去碰请求流,框架内部已经处理得当。

方案二:如果确实需要自定义(比如要记录完整 body),必须正确实现可重复读的包装器:

public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
    private final byte[] cachedBody;

    public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
        super(request);
        // 一次性读出来缓存住
        this.cachedBody = StreamUtils.copyToByteArray(request.getInputStream());
    }

    @Override
    public ServletInputStream getInputStream() {
        // 每次都返回一个新的、基于缓存的流
        ByteArrayInputStream bais = new ByteArrayInputStream(cachedBody);
        return new ServletInputStream() {
            public int read() { return bais.read(); }
            public boolean isFinished() { return bais.available() == 0; }
            public boolean isReady() { return true; }
            public void setReadListener(ReadListener l) {}
        };
    }

    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }
}

关键在于 getInputStream() 每次返回一个新的、基于缓存字节数组的流,而不是返回原来那个。

延伸与预防

核心原则:拦截请求的组件一旦读过 body,就必须负责把它还原。

写过滤器 / 拦截器 / 中间件的时候,只要碰到请求或响应流,就要先想清楚一个问题:"我读完之后,后面的人还能不能读?" 如果答案是不确定,那就不要碰这个流。

同类问题不只出现在请求体上:包装响应流(为了记录返回值)如果处理不当,会导致返回内容为空或被截断;把流提前关闭,会让后面的组件报"流已关闭"。这些都属于"谁碰流、谁负责"的范畴。

另外一条务实建议:能用框架自带的能力就不要自己造轮子,尤其是在这种涉及底层流的组件上。框架作者已经把这类坑踩过一遍了,自己再实现一次的收益通常小于风险。如果需要的能力框架没有,那也尽量找经过检验的第三方库,而不是手写。

最后给一条自查清单:写任何拦截组件之前,先问三句——它读请求体吗?读响应体吗?读完会还回去吗?三句里有任何一句答不上来,就先别把它放进链路。把这条列进代码评审的检查项,比事后从"请求体缺失"倒推原因划算得多。

评论(0)

  • 还没有评论,来抢沙发~

相关文章