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

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 本文主要是分析生产遇到的一个问题,然后开始探究原因,开始的时候发现是 Spring 的原因,因为使用 Map 接收时, headerName 什么格式就是什么格式。在自己写 demo 时又发现,原来和 Spring 的关系并不大,是容器的原因。不同的容器处理方式不同。所以总结出来相关文章,供大家参考,不足之处,欢迎指正。

网络异常,图片无法展示
|


前言


部署测试,部署预发布,一切测试就绪,上生产。

发布生产

闪退

What???

马上回滚

开始排查

后端一模一样的代码,不是 APP 端的问题吧。可 APP 端没有发版啊。

…… 一番排查

原来是 APP 端打包,测试和预发布包 Header 传的都是 Authorization ,生产传的是 authorization 。就是大小写问题,那赶紧改。


背景


首页接口只有登录才可以进入,因为首页要展示获取用户账户的一些信息。这里使用的是统一拦截,从 Header 中获取 token 后,使用 token 获取用户信息。

而现在要改为用户未登录也可以查看首页信息中的宣传文案等等,只不过账户信息不显示。


原流程

网络异常,图片无法展示
|


整个过程代码在 ThreadLocal底层原理 里面有所介绍。这里省略一部分代码。

@Component
public class TokenInterceptor implements HandlerInterceptor {
    @Override
    public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3)
            throws Exception {
        LocalUserUtils.remove();
    }
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 请求方法是否存在注解
        boolean assignableFrom = handler.getClass().isAssignableFrom(HandlerMethod.class);
        if (!assignableFrom) {
            return true;
        }
        CheckToken checkToken = null;
        if (handler instanceof HandlerMethod) {
            checkToken = ((HandlerMethod) handler).getMethodAnnotation(CheckToken.class);
        }
        // 没有加注解 直接放过
        if (checkToken == null) {
            return true;
        }
        // 从Header中获取Authorization
        String authorization = request.getHeader("Authorization");
        log.info("header authorization : {}", authorization);
        if (StringUtils.isBlank(authorization)) {
            log.error("从Header中获取Authorization失败");
            throw CustomExceptionEnum.NOT_HAVE_TOKEN.throwCustomException();
        }
        // 其他代码省略
        return true;
    }
}

从代码中可以看出这里大概过程如下:

  1. 是使用拦截器拦截请求
  2. 如果方法没有 CheckToken 注解直接放过
  3. 有 CheckToken 注解,则从 request 的 header 中获取 Authorization


新需求

这里想到只需要把注解去掉,然后从请求参数中获取 token 即可。获取到走原逻辑,获取不到则只返回宣传文案等信息。


从 Header 中获取信息


直接获取请求头某一个 headerName

@PostMapping("/getAuthorizationByKey")
public String getAuthorizationByKey(@RequestHeader("Authorization") String authorization) {
    log.info("获取 Authorization --->{}", authorization);
    return authorization;
}


使用 Map 获取所有请求头

@PostMapping("/getAuthorizationByMap")
public String getAuthorizationByMap(@RequestHeader Map<String, String> map) {
    String authorization = map.get("Authorization");
    log.info("获取 Authorization --->{}", authorization);
    return authorization;
}


使用 MultiValueMap 获取请求头

@PostMapping("/getAuthorizationByMultiValueMap")
public String getAuthorizationByMultiValueMap(@RequestHeader MultiValueMap<String, String> map) {
    List<String> authorization = map.get("Authorization");
    log.info("获取 Authorization --->{}", authorization);
    return "SUCCESS";
}


使用 HttpHeaders 获取请求头

@PostMapping("/getAuthorizationByHeaders")
public String getAuthorizationByHeaders(@RequestHeader HttpHeaders headers) {
    List<String> authorization = headers.get("Authorization");
    log.info("获取 Authorization --->{}", authorization);
    return "SUCCESS";
}


使用 HttpServletRequest 获取

@PostMapping("/getAuthorizationByServlet")
public String getAuthorizationByServlet(HttpServletRequest request) {
    String authorization = request.getHeader("Authorization");
    log.info("获取 Authorization --->{}", authorization);
    return authorization;
}


测试文件

网络异常,图片无法展示
|


经过测试这些都是可以的,最终选择使用 Map 接收 Header ,然后从 Map 中获取 Authorization。

PS: 可能有小伙伴测试不过,发现接受的 header 里的 name 全都是小写了,可以继续阅读。 源码在文末,也可以关注公众号,发送 headerName/4 获取。

网络异常,图片无法展示
|

你以为事情如果到这里就结束了,那真是太天真了。

这不,出现了文章开头的描述的场景,赶紧回滚,然后排查问题,最后定位到是 Header 的 name 大小写问题。


思考

  1. 之前 APP 端也是这么传的,那为什么使用拦截器是正常的呢?
  2. 上面的那几种方式是不是都是这样?
  3. 不排除 tomcat 发现原来都会转换为小写,又是为什么?


模拟排查


环境配置

模拟生产首先使用相同的容器配置,这里排除了内置的 tomcat 容器,并且使用 undertow 容器。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
        <!-- Exclude the Tomcat dependency -->
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>


使用拦截器传小写为什么没有问题

  • 修改使用小写 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 方法:

private static int higher(byte b) {
    return b & (b >= 'a' && b <= 'z' ? 0xDF : 0xFF);
}


这块的含义

  1. 如果 b 是小写字符则 b & 0xDF
  2. 如果 b 是大写字符则 b & 0xFF

对照 ASCII 表,大小写字母相差 32 而 0xFF(255) 和 0xDF(223) 同样相差 32,所以问题定位到了。header 的 name 无论是大写还是小写,都会查出同一个值。

当然你也可以这么传

网络异常,图片无法展示
|

这样的话谁在上面,Header 中使用的 name 就是那个。


使用 Map 为什么会区分大小写

传入的是大写

HttpServlet 
-> DispatcherServlet#doDispatch 
-> AbstractHandlerMethodAdapter#handle 
-> RequestMappingHandlerAdapter#handleInternal 
-> RequestMappingHandlerAdapter#invokeHandlerMethod 
-> ServletInvocableHandlerMethod#invokeAndHandle
-> InvocableHandlerMethod#invokeForRequest (解析参数值)
-> InvocableHandlerMethod#getMethodArgumentValues
-> RequesteaderMapMethodArgumentResolver#resolveArgument

网络异常,图片无法展示
|

如图所示 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#getHeaderorg.apache.catalina.connector.Request#getHeaderorg.apache.coyote.Request#getHeaderorg.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:不同的容器下实现方式不同,这里列表说明

undertow tomcat jetty
请求参数大小写转换 不变 小写 驼峰
直接获取请求头某一个 headerName 忽略大小写,不能为空 忽略大小写,不能为空 忽略大小写,不能为空
使用 Map 获取所有请求头 Map 的 key 和传入 headerName 大小写的一致,保持一致可获取到 Map 的 key 全是小写,需要使用小写headerName 获取 Map 的 key 是驼峰命名法,要使用驼峰命名才可以获取到
使用 MultiValueMap 获取请求头 实际是从 LinkedHashMap 中获取,区分大小写 实际是从 LinkedHashMap 中获取,区分大小写 从 LinkedCaseInsensitiveMap 获取,不区分大小写
使用 HttpHeaders 获取请求头 从 LinkedCaseInsensitiveMap 获取,不区分大小写 从 LinkedCaseInsensitiveMap 获取,不区分大小写 从 LinkedCaseInsensitiveMap 获取,不区分大小写
使用 HttpServletRequest 获取 使用 HttpString.hashCodeOf(headerName) 忽略了大小写 调用 MimeHeaders#getValue 忽略了大小写 HttpFields#get 忽略了大小写

通过表格发现,即使是不同的容器在使用 HttpHeaders 获取请求头是都是调用了 Spring 的 LinkedCaseInsensitiveMap 获取 header,并且内部忽略了大小写,这里比较推荐使用。

同样使用 HttpServletRequest 的方式获取也比较推荐。



结束语

本文主要是分析生产遇到的一个问题,然后开始探究原因,开始的时候发现是 Spring 的原因,因为使用 Map 接收时, headerName 什么格式就是什么格式。

在自己写 demo 时又发现,原来和 Spring 的关系并不大,是容器的原因。不同的容器处理方式不同。所以总结出来相关文章,供大家参考,不足之处,欢迎指正。

目录
相关文章
|
4月前
|
Go 开发者
【应用服务 App Service】App Service发生错误请求时,如何查看IIS Freb日志,从中得知错误所发生的模块,请求中所携带的Header信息
【应用服务 App Service】App Service发生错误请求时,如何查看IIS Freb日志,从中得知错误所发生的模块,请求中所携带的Header信息
|
5月前
|
消息中间件 Android开发 开发者
🔍深度剖析Android内存泄漏,让你的App远离崩溃边缘,稳如老狗!🐶
【7月更文挑战第28天】在 Android 开发中,内存管理至关重要。内存泄漏可悄无声息地累积,最终导致应用崩溃或性能下滑。它通常由不正确地持有 Activity 或 Fragment 的引用引起。常见原因包括静态变量持有组件引用、非静态内部类误用、Handler 使用不当、资源未关闭及集合对象未清理。使用 Android Studio Profiler 和 LeakCanary 可检测泄漏,修复方法涉及使用弱引用、改用静态内部类、妥善管理 Handler 和及时释放资源。良好的内存管理是保证应用稳定性的基石。
109 4
|
4月前
|
存储 大数据 索引
【Azure Contianer Apps】在云上使用容器应用时收集日志遇见延迟问题
【Azure Contianer Apps】在云上使用容器应用时收集日志遇见延迟问题
|
4月前
|
Web App开发 安全 JavaScript
【Azure 应用服务】App Service 通过配置web.config来添加请求返回的响应头(Response Header)
【Azure 应用服务】App Service 通过配置web.config来添加请求返回的响应头(Response Header)
|
4月前
|
Web App开发 iOS开发
【Azure 应用服务】App Service站点Header头中的中文信息显示乱码?当下载文件时,文件名也是乱码?
【Azure 应用服务】App Service站点Header头中的中文信息显示乱码?当下载文件时,文件名也是乱码?
|
4月前
|
XML 数据格式
【应用服务 App Service】如何移除App Service Response Header中包含的服务器敏感信息
【应用服务 App Service】如何移除App Service Response Header中包含的服务器敏感信息
|
API Android开发
fragment不断切换app崩溃的解决办法
fragment不断切换app崩溃的解决办法
|
运维 监控 Cloud Native
《云原生架构容器&微服务优秀案例集》——02 汽车/制造——极氪汽车 APP全面升级云原生技术架构,高效提升用户出行体验
《云原生架构容器&微服务优秀案例集》——02 汽车/制造——极氪汽车 APP全面升级云原生技术架构,高效提升用户出行体验
355 0
|
移动开发 iOS开发
h5的header在iOS app内不显示,在浏览器显示正常解决办法
h5的header在iOS app内不显示,在浏览器显示正常解决办法
131 0
|
Java Android开发 UED
能否让APP永不崩溃—小光和我的对决
关于拦截异常,想必大家都知道可以通过Thread.setDefaultUncaughtExceptionHandler来拦截App中发生的异常,然后再进行处理。
222 0
能否让APP永不崩溃—小光和我的对决