APP 莫名崩溃,开始以为是 Header 中 name 大小写的锅,最后发现原来是容器的错!

前言
部署测试,部署预发布,一切测试就绪,上生产。
发布生产
闪退
What???
马上回滚
开始排查
后端一模一样的代码,不是 APP 端的问题吧。可 APP 端没有发版啊。
…… 一番排查
原来是 APP 端打包,测试和预发布包 Header 传的都是
Authorization
,生产传的是authorization
。就是大小写问题,那赶紧改。
公众号:liuzhihangs,记录工作学习中的技术、开发及源码笔记;时不时分享一些生活中的见闻感悟。欢迎大佬来指导!
背景
首页接口只有登录才可以进入,因为首页要展示获取用户账户的一些信息。这里使用的是统一拦截,从 Header 中获取 token 后,使用 token 获取用户信息。
而现在要改为用户未登录也可以查看首页信息中的宣传文案等等,只不过账户信息不显示。
原流程

整个过程代码在 ThreadLocal底层原理 里面有所介绍。这里省略一部分代码。
从代码中可以看出这里大概过程如下:
是使用拦截器拦截请求
如果方法没有 CheckToken 注解直接放过
有 CheckToken 注解,则从 request 的 header 中获取 Authorization
新需求
这里想到只需要把注解去掉,然后从请求参数中获取 token 即可。获取到走原逻辑,获取不到则只返回宣传文案等信息。
从 Header 中获取信息
直接获取请求头某一个 headerName
使用 Map 获取所有请求头
使用 MultiValueMap 获取请求头
使用 HttpHeaders 获取请求头
使用 HttpServletRequest 获取
测试文件

经过测试这些都是可以的,最终选择使用 Map 接收 Header ,然后从 Map 中获取 Authorization。
PS: 可能有小伙伴测试不过,发现接受的 header 里的 name 全都是小写了,可以继续阅读。
源码在文末,也可以关注公众号,发送 headerName/4 获取。

你以为事情如果到这里就结束了,那真是太天真了。
这不,出现了文章开头的描述的场景,赶紧回滚,然后排查问题,最后定位到是 Header 的 name 大小写问题。
思考
之前 APP 端也是这么传的,那为什么使用拦截器是正常的呢?
上面的那几种方式是不是都是这样?
不排除 tomcat 发现原来都会转换为小写,又是为什么?
模拟排查
环境配置
模拟生产首先使用相同的容器配置,这里排除了内置的 tomcat 容器,并且使用 undertow 容器。
使用拦截器传小写为什么没有问题
修改使用小写
authorization

debug 断点

神奇的一幕出现了,收到的确实是小写,但是 request.getHeader("Authorization"); 却可以获取到 authorization
F7 继续往里跟

io.undertow.servlet.spec.HttpServletRequestImpl#getHeader
第 190 行,从 HeaderMap 中获取第一个元素

io.undertow.util.HeaderMap#getFirst
第 297 行, 通过 getEntry 方法获取 header

继续追踪,发现在 io.undertow.util.HeaderMap#getEntry(java.lang.String)
方法 77~79 行的时候获取到了 header 信息。那就看一下这块的源码吧。
在仔细看一下发现是 77 行 final int hc = HttpString.hashCodeOf(headerName);
在获取 name 的 hashCode 时,这里无论大小写,都是同一个 hashCode。这块代码如下

higher 方法:
这块的含义
如果 b 是小写字符则
b & 0xDF
如果 b 是大写字符则
b & 0xFF
对照 ASCII 表,大小写字母相差 32 而 0xFF(255) 和 0xDF(223) 同样相差 32,所以问题定位到了。header 的 name 无论是大写还是小写,都会查出同一个值。
当然你也可以这么传

这样的话谁在上面,Header 中使用的 name 就是那个。
使用 Map 为什么会区分大小写
传入的是大写

如图所示 String headerName = iterator.next();
name 被区分大小写放到了 LinkedHashMap 中,后续会反射调用对应的 Controller 方法。
所以也就出现了我所遇到的问题。
当然理论上 APP 客户端不应该测试和预发布使用大写,而生产使用小写。
上面阅读的源码只是 Spring 对 Header 的处理,Spring 在 HttpServlet
收到请求时,Spring 没有对请求 Header 的 name 大小写进行转换,只是在获取对应 value 的时候,没有区分大小写进行获取。

容器对 header 的处理
undertow 容器的处理
请求参数的处理
这里发现 undertow 并没有对请求参数进行大小写转换处理操作。
从 HttpServletRequest 获取 header
debug 发现调用的是 io.undertow.servlet.spec.HttpServletRequestImpl#getHeader
,这个过程就是上面的排查过程。
从 Headers 中获取 header
通过 debug 发现 jetty 调用的是 org.springframework.http.HttpHeaders#get
,然后调用 org.springframework.util.MultiValueMapAdapter#get
,然后调用 org.springframework.util.LinkedCaseInsensitiveMap#get

这里会不区分大小写
从 MultiValueMap 获取 header
这块 debug 发现是直接从 LinkedHashMap
获取的,所以区分了大小写。
tomcat 容器的处理
请求参数的处理
而如果没有排除的话,即使用内嵌的 tomcat 容器无论传递大写还是小写,接收到的全部都是小写,又是怎么个情况呢?
通过 debug 发现没有排除 tomcat 使用的是,在接收请求时使用的是 org.apache.coyote.http11.Http11Processor
。
在 Http11Processor#service
方法中

类 284 行负责处理解析 header
进入 org.apache.coyote.http11.Http11InputBuffer#parseHeaders
方法

第 589 行 (Download Sources 后),阅读 parseHeader
方法

发现会将请求 header 的 name 转换为小写
从 HttpServletRequest 获取 header
当使用 tomcat 容器时,调用 org.apache.catalina.connector.RequestFacade#getHeader
, org.apache.catalina.connector.Request#getHeader
, org.apache.coyote.Request#getHeader
org.apache.tomcat.util.http.MimeHeaders#getHeader
最后调用 org.apache.tomcat.util.http.MimeHeaders#getValue
获取 header

这里也会忽略大小写判断
从 Headers 获取 header
通过 debug 发现 tomcat 容器下调用的是 org.springframework.http.HttpHeaders#get
,然后调用 org.springframework.util.MultiValueMapAdapter#get
,然后调用 org.springframework.util.LinkedCaseInsensitiveMap#get

这里会不区分大小写
从 MultiValueMap 获取 header
这块 debug 发现是直接从 LinkedHashMap
获取的,所以区分了大小写。
jetty 容器的处理
请求参数的处理
如果换成 jetty 容器的话
在 org.eclipse.jetty.server.HttpConnection
中又会发现无论传入大写还是小写都会被转换为驼峰。
源码可以阅读 org.eclipse.jetty.http.HttpParser#parseFields

会转换为驼峰命名法。
从 HttpServletRequest 获取 header
通过 debug 发现 jetty 调用的是 org.eclipse.jetty.server.Request#getHeader

jetty 在获取 header 时,会调用 org.eclipse.jetty.http.HttpFields#get


原来在获取的时候忽略了大小写
从 Headers 获取 header
通过 debug 发现 jetty 容器下调用的是 org.springframework.http.HttpHeaders#get
,然后调用 org.springframework.util.MultiValueMapAdapter#get
,然后调用 org.springframework.util.LinkedCaseInsensitiveMap#get

这里会不区分大小写
从 MultiValueMap 获取
也是调用的 org.springframework.util.MultiValueMapAdapter#get
然后不区分大小写。和从 Headers 中获取相同。
总结
Q&A
Q: 为什么拦截器获取 Authorization 可以不区分大小写?
A: 从拦截器获取 Authorization 其实就是从 HttpServletRequest
中获取,这里无论使用 tomcat 还是使用 undertow 或者 jetty 获取 Header 是都是忽略 headerName 的大小写的。具体可以阅读上面的源码分析。
Q: 这么多获取 Header 的方式有什么区别?
A:
不同的容器下实现方式不同,这里列表说明

通过表格发现,即使是不同的容器在使用 HttpHeaders 获取请求头是都是调用了 Spring 的 LinkedCaseInsensitiveMap
获取 header,并且内部忽略了大小写,这里比较推荐使用。
同样使用 HttpServletRequest 的方式获取也比较推荐。
结束语
本文主要是分析生产遇到的一个问题,然后开始探究原因,开始的时候发现是 Spring 的原因,因为使用 Map 接收时, headerName 什么格式就是什么格式。
在自己写 demo 时又发现,原来和 Spring 的关系并不大,是容器的原因。不同的容器处理方式不同。所以总结出来相关文章,供大家参考,不足之处,欢迎指正。
相关资料
本文源码地址:https://github.com/liuzhihang/header-demo
版权声明: 本文为 InfoQ 作者【程序员小航】的原创文章。
原文链接:【http://xie.infoq.cn/article/2432dc4520078847f3a1bcc66】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论