平常我们在做应用层开发时,很少会注意到 HTTP 请求的大小写敏感问题。笔者在围观一个 HTTP 服务端 Header 的解析程序时,发现了一个微妙的 bug。这篇笔记是针对该问题的一个有趣的探究。
比如遇到下面的请求,你能直接说出正在使用框架的默认行为吗?
Host: example.com
accept-encoding: gzip, deflate
Accept-Language: en-us
fOO: Bar
foo: two
1 问题引入
range
请求是只请求一部分 HTTP 文件的请求。在收取大长度媒体文件的场景非常常见。
在线上观测到了部分此类超出预期的请求,导致返回异常。
GET /z4d4kWk.mp4 HTTP/1.1
Host: example.
Range: bytes=0-1023
range: bytes=0-1
很明显,服务端应当正确处理这种 Header 重复的现象。
2 HTTP 标准?
根据[1]讨论串和 RFC 整理。
之所以打一个问号,是因为浏览器和前端的使用习惯[1]、具体实现[5],我们可能偶尔在实际中遇到该类疑难杂症。
HTTP/1.1
在 RFC 7230 (Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing) 中,规定了 Header Name 是大小写不敏感 case-insensitive 的。
Each header field consists of a case-insensitive field name followed
by a colon (":"), optional leading whitespace, the field value, and
optional trailing whitespace.
HTTP/2
在 RFC 7540 中,规定了同样是大小写不敏感的。
Just as in HTTP/1.x, header field names are strings of ASCII characters that are compared in a case-insensitive fashion.
但值得指出的是,针对应用层的使用,标准如此规定。在 HTTP/2 协议内部,编码前需要统一转化为小写。
However,header field names MUST be converted to lowercase prior to their encoding in HTTP/2.
A request or response containing uppercase header field names MUST be treated as malformed.
之所以这么处理,推测是送进 HPACK
这种利用字典的 Header 压缩算法前,消除大小写的分歧。
3 Go HTTP 包的处理
Go 的 net/http
标准库使用广泛。注意,其默认使用 CanonicalHeaderKey
模式处理接收和请求的 Header Name。即令第一个字母和横杠后的字母为大写,剩下字母为小写。
其实在官方文档中已经明确示例了处理模式[6]。
接收的 Header
// If a server received a request with header lines,
//
// Host: example.com
// accept-encoding: gzip, deflate
// Accept-Language: en-us
// fOO: Bar
// foo: two
//
// then
//
// Header = map[string][]string{
// "Accept-Encoding": {"gzip, deflate"},
// "Accept-Language": {"en-us"},
// "Foo": {"Bar", "two"},
// }
//
// For incoming requests, the Host header is promoted to the
// Request.Host field and removed from the Header map.
//
// HTTP defines that header names are case-insensitive. The
// request parser implements this by using CanonicalHeaderKey,
// making the first character and any characters following a
// hyphen uppercase and the rest lowercase.
发送的 Header (Header的增改)
涉及 Add
Get
Set
方法,根据文档描述和查看源码,都是默认通过 textproto.CanonicalMIMEHeaderKey.
进行了处理。
作为开发者需要注意
CanonicalHeaderKey
处理是默认的。因此,在使用 net/http
处理 7 层转发时,要注意这个特性。
笔者之所以没有在测试环境的镜像流量中发现开篇的疑难问题,就是因为开发的 7 层镜像转发程序工具默认处理了这种接收请求 Header。
4 结语
虽然标准规定了 Header Name 是大小写不敏感的。但或由于浏览器和前端的使用习惯[1],或由于具体实现[5],我们排查某些疑难杂症的时候也可以考虑该问题。
比如案例 [5] 中,上游 api 并没有按照标准处理大小写,导致业务异常。
如果读者能对 CanonicalHeaderKey
有印象,本篇笔记将非常荣幸。
Extra
讨论串[1]也有回答提示,虽然 header name 是大小写无关的,但 HTTP 方法是大小写敏感的。所以我们在使用 curl 时,基本都会强制输入方法为大写。如 POST
, GET
等等。
# RFC 7230
## 3.1.1. Request Line
...
The method token indicates the request method to be performed on the
target resource. The request method is case-sensitive.
参考资料
[1] Are HTTP headers case-sensitive? - StackOverflow
[2] RFC 7230
[3] RFC 7540
[4] HPACK: the silent killer (feature) of HTTP/2 - Cloudflare Blog
[5] AWS API Gateway Custom Authorization header case sensitivity
[6] https://pkg.go.dev/net/http#Request.Header