起缘

最近公司跟美图对接DSP系统, 我们公司使用的是Java语言的 Spring Boot 框架, 美图给的是一个 Google 的 proto 文件.

对接的时候, 我们在Spring的 Controller 里使用

public Object recBid(HttpServletRequest request) throws IOException {
        InputStream is = request.getInputStream();
        OpenRtb.BidRequest bidRequest = OpenRtb.BidRequest.parseFrom(is);
        ...
}

不过, 问题来了. 美图那边的技术人员说他们发的HTTP请求, 是没有 Content-Type: application/x-protobuf 请求头(或类似).

但是, 我们发现, 接收的 inputStream 一下没数据!!!

调试

远程调试的时候发现, 美图那边发过来的请求的 Content-Typeapplication/x-www-form-urlencoded 并且报如下的异常

img

然后我自己新建了一个裸的 Spring Boot 项目来接收这个, 并用内置的Tomcat来启动, 却是没问题的.(我的天….)

img

这个问题我纠结了好几天, 最后记忆中隐约记得以前看过 Servlet 规范文档, 记得会有些情况下, 会导致 request 对象中的 inputStream 会被清空的.

所以, 就按这个思路继续排查.

原因

Java Servlet 3.1 规范笔记

赶紧翻了以前写的博客, 还好记得有笔记.

Post表单时要满足以下条件时 parameter 集合才可用

请求是 http 或 https 请求的方法是 POST content type 为: application/x-www-form-urlencoded servlet 已经在 request 对象上调用了相关的 getParameter 方法。 当以上条件不满足时,POST 表单的数据并不会设置到 parameter 集合中,但依然可以通过 request 对象的 inputstream 来获取。 当以上条件满足时,POST 表单的数据在 request 对象的 inputstream 将不再可用了。

注: 美图那边发过来的就是 http + post + application/x-www-form-urlencoded

那也就是说, 肯定是在进入到 Controller 之前, 已经有其他地方调用过了 getParameter 方法, 才会导致 inputStream 不可用了. 这个极其重要的线索, 指导了我. 所以, 我在调试的时候, 在 request 对象的所有 getParameter 相关的地方, 都打上了断点, 看看到底是哪里导致这个问题的.

最后发现

img

是在 HiddenHttpMethodFilter 这个 filter 里调用了.

Spring自带的 filters 有:

img

其中, 会调用 getParameters 方法的有:

HttpPutFormContentFilterHiddenHttpMethodFilter

至于为什么 Spring Boot 会在使用外部Tomcat时自动添加这两个 filter, 这个原因还没有仔细研究. 但使用内置的Tomcat启动时, 却发现没自动注册这些 filters .

解决

知道了原因, 那解决它就容易了. 如果实在不需要这些 filters 的话, 那就直接禁用这两个 filter 就可以了. 在Spring Boot 的配置里添加以下配置:

    @Bean
    public HttpPutFormContentFilter httpPutFormContentFilter() {
        return new HttpPutFormContentFilter();
    }

    @Bean
    public FilterRegistrationBean disableSpringBootHttpPutFormContentFilter(HttpPutFormContentFilter filter) {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(filter);
        filterRegistrationBean.setEnabled(false);
        return filterRegistrationBean;
    }

    @Bean
    public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
        return new HiddenHttpMethodFilter();
    }

    @Bean
    public FilterRegistrationBean disableSpringBootHiddenHttpMethodFilter(HiddenHttpMethodFilter filter) {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(filter);
        filterRegistrationBean.setEnabled(false);
        return filterRegistrationBean;
    }

即可禁用这两个 filters.

这时再测试, 发现可以正确接收 inputStream 对象了.

另类解决

因为我们系统是使用 nginx 做代理的, 最简单的方式, 其实就是在 nginx 代理的时候, 直接让 nginx 设置一下相应的 content-type 请求头就可以了.