Tomcat 中 HTTP 400 Bad Request 的常见原因以及示例

背景

在web开发中, HTTP 400 Bad Request 是一种常见但却难以定位的问题, 通常是因为请求没有遵循 HTTP 标准. 原因看起来很直观, 但是在定位此类问题时, 往往需要花费极大的精力. 有以下几个难点:

  • 一个请求在从客户端发出到应用服务器收到这个过程中, 中间经历了各种转发, 每个中间环节都有可能对请求进行改写, 例如代理、LB等. 所以通常需要与多个部门沟通协作, 定位改动发生的环节.
  • 当不满足HTTP 标准的请求到达应用服务器之后, Tomcat 会在请求进入业务逻辑之前就返回400, 并且对于这种行为的默认的日志等级为debug. 这就意味着, 当此类问题发生时, 从业务逻辑的日志中发现不了任何异常. 通常需要抓包或暂时将Tomcat 的日志标准设置为debug.
  • 即使抓包获取了请求信息, 也很难直接观察出请求出问题的地方.
  • 甚至有些情况下, “相同”的请求, 一部分没有问题, 另一部分有问题. 这给定位问题带来了更多的干扰项.

在本片文章中, 会分享实际生产环境下的真实事例. 并且对Tomcat中所有400的情况给出了实际例子, 方便以后更直观的判断请求是否有问题.

本文基于 tomcat-embed-core-9.0.29

案例分析

Case 1. Web容器使用的协议版本不同

详见下一篇博文

Case 2. 请求头中含有非法字符

现象

由于对LB进行升级, 将流量从旧的LB切换到新的LB之后, 某些客户端的所有响应都变成了 400 BAD REQUEST.

分析

  1. 切换前后的网络拓扑结构发生了变化:

    1. 切换之前, 请求路线为 client -> 旧LB -> proxy -> server
    2. 切换之后, 请求路线为 client -> 新LB -> server
    3. proxy 做了一些额外的操作来确保请求的正确性. 在这里, 它将请求头中冒号前的空格给删除了.
  2. 请求头中冒号前的空格属于非法字符, Tomcat对于这种情况会直接返回400.

客户端发送的请求头示例 "APPLICATION-VERSION : 519", 冒号前的空格即为非法字符.

Tomcat相关源码

Tomcat调用逻辑如下. 在parseHeader()方法中, 会使用isToken()方法检查请求头中是否有非法字符.

org.apache.coyote.http11.Http11Processor#service

org.apache.coyote.http11.Http11InputBuffer#parseHeaders

org.apache.coyote.http11.Http11InputBuffer#parseHeader

org.apache.tomcat.util.http.parser.HttpParser#isToken

org.apache.coyote.http11.Http11InputBuffer#skipLine

throw new IllegalArgumentException(message);

response.setStatus(400);


org.apache.coyote.http11.Http11Processor#service: parseHeaders and catch IllegalArgumentException


image

org.apache.coyote.http11.Http11InputBuffer#parseHeader: 检验请求头并在skipLine()中抛出llegalArgumentException. IS_TOKEN 是一个用来存储非法字符的位图, 非法字符包含 ' ' ',' '(' ')' '' 等.

image

org.apache.coyote.http11.Http11InputBuffer#skipLine: 抛出异常

image

Tomcat HTTP 400 Bad Request总结

* Guide

这里总结了所有tomcat-embed-core-9.0.29中会返回HTTP 400 Bad Request的情况, 并给出了样例.

以下例子均为HTTP报文格式, case 0 是一个可以正确返回200的请求, 后续的例子是在case 0 的基础上进行改变, 改变的部分会以橙色标出, 导致400的关键字会以红色标出, 便于直观的发现问题.

0. 正确的请求

200 Sample

Host: localhost
Content-Type: application/xml
X-SOA-OPERATION-NAME: getVersion\r\n\tsecond line\r\n\tthird line
X-SOA-SERVICE-NAME: CoreShippingService
Content-length: {requestBodyLength}

200 Sample

Host: localhost
Content-Type: application/xml
X-SOA-OPERATION-NAME: getVersion\r\n\tsecond line\r\n\tthird line
X-SOA-SERVICE-NAME: CoreShippingService
Content-length: {requestBodyLength}

1. 请求头Host中端口号不为数字

org.apache.coyote.AbstractProcessor#line 302

400 Sample

Host: localhost:abc
Content-Type: application/xml
X-SOA-OPERATION-NAME: getVersion\r\n\tsecond line\r\n\tthird line
X-SOA-SERVICE-NAME: CoreShippingService
Content-length: {requestBodyLength}

2. 请求头Host中含有非法字符

org.apache.coyote.AbstractProcessor#line 337

Illegal characters logic: org.apache.tomcat.util.http.parser.HttpParser.DomainParseState#next

400 Sample

Host: localho(st
Content-Type: application/xml
X-SOA-OPERATION-NAME: getVersion\r\n\tsecond line\r\n\tthird line
X-SOA-SERVICE-NAME: CoreShippingService
Content-length: {requestBodyLength}

3.Socket状态为CONNECT_FAIL(eg. TLS handshake FAIL)

org.apache.coyote.AbstractProcessor#line 982

No example

4. 请求头名称中含有非法字符或空格

org.apache.coyote.http11.Http11Processor#line 311

Character set: org.apache.tomcat.util.http.parser.HttpParser#IS_TOKEN

400 Sample for illegal character

Host: localhost
Content-T(ype: application/xml
X-SOA-OPERATION-NAME: getVersion\r\n\tsecond line\r\n\tthird line
X-SOA-SERVICE-NAME: CoreShippingService
Content-length: {requestBodyLength}

400 Sample for blank

Host: localhost
Content-Type : application/xml
X-SOA-OPERATION-NAME: getVersion\r\n\tsecond line\r\n\tthird line
X-SOA-SERVICE-NAME: CoreShippingService
Content-length: {requestBodyLength}

5. 存在多个Host请求头

org.apache.coyote.http11.Http11Processor#line 609

400 Sample

Host: localhost
Host: localhost
Content-Type: application/xml
X-SOA-OPERATION-NAME: getVersion\r\n\tsecond line\r\n\tthird line
X-SOA-SERVICE-NAME: CoreShippingService
Content-length: {requestBodyLength}

6. 不存在Host请求头

org.apache.coyote.http11.Http11Processor#line 612

400 Sample

Host: localhost
Content-Type: application/xml
X-SOA-OPERATION-NAME: getVersion\r\n\tsecond line\r\n\tthird line
X-SOA-SERVICE-NAME: CoreShippingService
Content-length: {requestBodyLength}

7. 授权中的用户信息含有非法字符

org.apache.coyote.http11.Http11Processor#line 660

Character set: org.apache.tomcat.util.http.parser.HttpParser#IS_USERINFO

400 Sample

Host: localhost
Content-Type: application/xml
X-SOA-OPERATION-NAME: getVersion\r\n\tsecond line\r\n\tthird line
X-SOA-SERVICE-NAME: CoreShippingService
Content-length: {requestBodyLength}

8. URL中的Host与请求头中的Host不一致

org.apache.coyote.http11.Http11Processor#line 686

400 Sample

Host: localhost
Content-Type: application/xml
X-SOA-OPERATION-NAME: getVersion\r\n\tsecond line\r\n\tthird line
X-SOA-SERVICE-NAME: CoreShippingService
Content-length: {requestBodyLength}

9. Start line中协议名称非法

org.apache.coyote.http11.Http11Processor#line 705

400 Sample

Host: localhost
Content-Type: application/xml
X-SOA-OPERATION-NAME: getVersion\r\n\tsecond line\r\n\tthird line
X-SOA-SERVICE-NAME: CoreShippingService
Content-length: {requestBodyLength}

10. URI 中含有非法字符

org.apache.coyote.http11.Http11Processor#line 713

Character set: org.apache.tomcat.util.http.parser.HttpParser#IS_ABSOLUTEPATH_RELAXED

400 Sample

Host: localhost
Content-Type: application/xml
X-SOA-OPERATION-NAME: getVersion\r\n\tsecond line\r\n\tthird line
X-SOA-SERVICE-NAME: CoreShippingService
Content-length: {requestBodyLength}

11. 请求头中Content-Length值不为数字

org.apache.coyote.http11.Http11Processor#line 739

400 Sample

POST /ws/spf HTTP/1.1
Host: localhost
Content-Type: application/xml
X-SOA-OPERATION-NAME: getVersion\r\n\tsecond line\r\n\tthird line
X-SOA-SERVICE-NAME: CoreShippingService
Content-length: abc

12. 请求头中含有多个Content-Length

org.apache.coyote.http11.Http11Processor#line 741

400 Sample

POST /ws/spf HTTP/1.1
Host: localhost
Content-Type: application/xml
X-SOA-OPERATION-NAME: getVersion\r\n\tsecond line\r\n\tthird line
X-SOA-SERVICE-NAME: CoreShippingService
Content-length: {requestBodyLength}
Content-length: {requestBodyLength}
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容