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}

你可能感兴趣的:(Tomcat 中 HTTP 400 Bad Request 的常见原因以及示例)