ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Tornado httputil 模块深入解析:HTTP 头部与 URL 操作实用工具全指南

2026/9/20 23:11:10 拓冰建站 浏览量
Tornado httputil 模块深入解析:HTTP 头部与 URL 操作实用工具全指南 Tornado httputil 模块深入解析HTTP 头部与 URL 操作实用工具全指南【免费下载链接】tornadoTornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed.项目地址: https://gitcode.com/gh_mirrors/to/tornado导读tornado.httputil是 Tornado 框架中客户端与服务端共享的 HTTP 底层工具模块负责 HTTP 请求/响应头部、URL、Cookie、表单与 multipart 上传体的解析与构造。它同时定义了HTTPServerRequest请求对象——该对象正是你在 RequestHandler.request 属性中拿到的那个对象。读完本文你将掌握HTTPHeaders的大小写不敏感字典用法、请求/响应起始行start line的解析、表单与文件上传体的安全解析配置以及这些工具在HTTPServer、AsyncHTTPClient和auth模块中的真实调用方式。本文内容以 docs/httputil.rst 的 API 参考为骨架结合 tornado/httputil.py 源码实现与 tornado/test/httputil_test.py 测试用例展开所有代码示例均可直接运行验证。模块定位客户端与服务端共享的 HTTP 基础层tornado/httputil.py模块的文档字符串开门见山HTTP utility code shared by clients and servers. This module also defines theHTTPServerRequestclass which is exposed viatornado.web.RequestHandler.request.这意味着该模块处于 Tornado 协议栈的底层位置承担三方面职责HTTP 消息建模HTTPHeaders头部集合、RequestStartLine/ResponseStartLine起始行、HTTPServerRequest请求对象、HTTPFile上传文件对象解析与构造头部文本解析、URL 拼接、表单/ multipart 体解析、Cookie 解析、时间戳格式化协议接口定义HTTPServerConnectionDelegate、HTTPMessageDelegate、HTTPConnection三个抽象接口构成 Tornado 4.0 起HTTPServer的插件化消息处理协议。从源码结构看该模块被广泛引用tornado/httpserver.py通过from tornado import httputil, iostream, netutil将其作为服务器核心依赖tornado/auth.py使用url_concat拼接 OAuth 等授权 URLtornado/routing.py依赖其请求对象完成路由匹配测试方面tornado/test/httputil_test.py提供了 774 行的专门测试覆盖。HTTPHeaders维护 Http-Header-Case 的头部字典核心行为大小写不敏感 键名规范化HTTPHeaders继承自collections.abc.MutableMapping是一个键自动规范化为Http-Header-Case格式的字典。规范化由_normalize_header函数实现lru_cache(1000) def _normalize_header(name: str) - str: Map a header name to Http-Header-Case. _normalize_header(coNtent-TYPE) Content-Type return -.join([w.capitalize() for w in name.split(-)])它使用lru_cache(1000)缓存最近 1000 次规范化结果避免高并发下重复计算。因此无论你写入content-type还是Content-Type读取时都能得到一致结果 from tornado.httputil import HTTPHeaders h HTTPHeaders({content-type: text/html}) list(h.keys()) [Content-Type] h[Content-Type] text/html多值支持add / get_list / get_all与普通字典不同HTTP 允许同一头部出现多次典型如Set-Cookie。HTTPHeaders提供了三个扩展方法add(name, value)为同一键追加新值get_list(name)返回该键的全部值列表get_all()返回所有(name, value)对的可迭代对象多值时产生多对。内部实现上HTTPHeaders使用self._as_list字段名到值列表的映射作为真实存储同时用self._combined_cache缓存合并值。常规字典接口如h[set-cookie]返回用逗号连接的合并值而get_list返回原始列表 h HTTPHeaders() h.add(Set-Cookie, AB) h.add(Set-Cookie, CD) h[set-cookie] AB,CD h.get_list(set-cookie) [AB, CD]这正是 HTTP 语义的精确复刻RFC 9110 规定字段值可用逗号合并但Set-Cookie是例外因此内部必须保留独立的字段行列表源码 tornado/httputil.py 中的注释明确说明了这一设计取舍。头部文本解析parse_line 与 parseparse_line(line)解析单行头部文本parse(headers)类方法解析完整头部块二者都返回/更新HTTPHeaders对象 h HTTPHeaders() h.parse_line(Content-Type: text/html) h.get(content-type) text/html h.parse_line(Content-Length: 42\r\n) h.get(content-type) text/html h HTTPHeaders.parse(Content-Type: text/html\r\nContent-Length: 42\r\n) sorted(h.items()) [(Content-Length, 42), (Content-Type, text/html)]值得注意的实现细节对应测试 tornado/test/httputil_test.py行折叠line folding以空格或制表符开头的续行会追加到上一行前置空白替换为单个空格。但若首行即以空白开头会抛出HTTPInputError(first header line cannot start with whitespace)\f换页符虽被str.isspace()视为空白但不符合 HTTP 规范同样会被拒绝行分隔符从 6.5 起同时接受 CRLF 与单独 LFRFC 9112 允许识别 LF 作为行终止符但裸 CR 不是合法分隔符测试test_optional_cr验证了这一点5.1 版本起格式错误的头部统一抛出HTTPInputError不再混用KeyError与ValueError6.5 版本起parse_line支持带或不带尾部 CRLF 的行因此AsyncHTTPClient的header_callback返回的头部行可直接喂给该方法。parse方法内部还有一个_chars_are_bytes标志在解析普通 HTTP 头部时按 latin-1 字节语义做 ABNF 严格校验而在解析multipart/form-data各部分头部时关闭字节级校验以允许 RFC 2231 非 ASCII 文件名源码 tornado/httputil.py 的注释详述了这一历史包袱。严格校验ABNF 规则与安全性HTTPHeaders的add方法在写入前会做双重校验键必须匹配_ABNF.field_nameRFC 9110 的 token 规则!#$%*-.^_|~0-9A-Za-z 字符集值必须匹配_ABNF.field_value或_FORBIDDEN_HEADER_CHARS_RE禁止\x00-\x08、\x0A-\x1F、\x7F等控制字符。测试test_invalid_header_names验证了空串、含空格/制表符/换行/\x00的键名、以及非 ASCII 字符é作为键名都会被拒绝并抛出HTTPInputError。测试test_forbidden_ascii_characters则遍历了 0xFF 以内的全部 ASCII 字符确认只有0x09TAB和0x20空格及可打印字符能进入头部值。这套校验机制是 Tornado 抵御 HTTP 响应拆分header injection类攻击的第一道防线。_ABNF内部类源码 tornado/httputil.py封装了 RFC 3986URI、RFC 9110HTTP Semantics、RFC 9112HTTP/1.1的 ABNF 规则子集以编译好的re.Pattern形式存在。值得留意的是uri_host对 IPv6 字面量做了简化处理——允许方括号和冒号出现在任意位置因为完整的 URI host ABNF 既复杂又不够严格。其他实用行为copy()与__copy__返回深一层副本HTTPHeaders(self)拷贝构造copy.copy和copy.deepcopy均被测试验证不共享内部列表支持pickle序列化往返测试test_pickle_roundtripstr(headers)输出符合 HTTP 报文格式的头部块每行Name: value\n可再通过HTTPHeaders.parse还原保证往返一致线性性能有保障测试test_linear_performance验证 10 万次add的耗时相对 1 万次呈线性增长lru_cache的规范化缓存与_combined_cache惰性合并功不可没。HTTPServerRequestWeb 层看到的请求对象HTTPServerRequest表示单个 HTTP 请求是tornado.web.RequestHandler的self.request属性所指向的对象自 Tornado 4.0 起从tornado.httpserver.HTTPRequest迁入本模块。核心属性一览属性类型说明methodstrHTTP 方法如GET、POSTuristr完整请求 URI含查询串pathstruri的路径部分?之前querystruri的查询串部分?之后versionstr请求指定的 HTTP 版本如HTTP/1.1headersHTTPHeaders请求头大小写不敏感且支持重复头bodybytes请求体字节串remote_ipstr客户端 IP若HTTPServer.xheaders开启则透传负载均衡器X-Real-Ip/X-Forwarded-For提供的真实 IP3.1 起支持X-Forwarded-For列表格式protocolstrhttp或httpsxheaders开启时透传X-Scheme头hoststr请求主机名通常取自Host头argumentsdictGET/POST 参数名称映射到字节串值列表区别于RequestHandler.get_argument返回的 unicode 字符串query_argumentsdict仅来自查询串的参数3.2 新增body_argumentsdict仅来自请求体的参数3.2 新增filesdict文件上传文件名映射到HTTPFile对象列表connectionHTTPConnection承载该请求的连接HTTP/1.1 下连接保持打开多个请求可顺序复用构造与 Host 校验HTTPServerRequest的构造参数全部可选method、uri、version、headers、body、host、files、connection、start_line、server_connection但源码中的弃用警告揭示了演化方向6.5.2 起host参数弃用改用headers[Host]6.6 起不带start_line参数创建对象会触发弃用警告6.7 将强制要求start_line一个RequestStartLine届时method/uri/version/host参数全部移除剩余参数变为仅关键字。测试test_default_constructor展示了新旧两种合法构造方式from tornado.httputil import HTTPServerRequest, RequestStartLine # 旧式已弃用仅测试中通过 ignore_deprecation 使用 HTTPServerRequest(methodGET, uri/) # 新式推荐 HTTPServerRequest(start_lineRequestStartLine(GET, /, HTTP/1.0))构造过程中的安全校验源码 tornado/httputil.pyHost头缺失时HTTP/1.0 请求回退为127.0.0.1其他版本抛出HTTPInputError(Missing Host header)Host值必须匹配_ABNF.host规则否则抛出HTTPInputErrorHost值含逗号时直接拒绝——这对应 RFC 9112 的要求代理可能把多个 Host 头合并成逗号分隔的单值RFC 9110 §5.3而逗号在 DNS 名中不允许出现因此以此规避多 Host 头攻击源码注释详细解释了这一技术性偏离 RFC 的取舍。常用方法与派生属性# 重建完整 URL request.full_url() # https://example.com/path?q1 # 请求耗时秒 request.request_time() # 请求开始到当前/完成的时间差 # 客户端 SSL 证书 request.get_ssl_certificate() # 返回 dict 或 None request.get_ssl_certificate(binary_formTrue) # 返回 DER 字节串get_ssl_certificate需要服务器端配置ssl.SSLContext的verify_mode如ssl.CERT_REQUIRED配合客户端证书认证才有效其实现通过connection.stream.socket.getpeercert()获取遇到SSLError时返回None源码 tornado/httputil.py 给出了完整的HTTPServer(app, ssl_optionsssl_ctx)配置示例。cookies属性是惰性计算的dict[str, http.cookies.Morsel]首次访问时用parse_cookie解析Cookie头再装入http.cookies.SimpleCookie键被SimpleCookie拒绝的非法 Cookie 会被静默丢弃。异常类型与协议接口HTTPInputError / HTTPOutputErrorHTTPInputError4.0 新增来自远程来源的格式错误 HTTP 请求/响应引发的异常。解析头部、起始行、查询串、表单体时的各种校验失败统一抛此异常HTTPOutputError4.0 新增HTTP 输出过程中的错误。三个协议接口4.0 起 HTTPServer 的扩展点Tornado 4.0 重构后HTTPServer通过tornado.httputil中定义的三个接口实现请求处理的完全可定制化参见 docs/releases/v4.0.0.rstHTTPServerConnectionDelegate服务器端连接生命周期接口。start_request(server_conn, request_conn)在服务器收到新请求时被调用返回一个HTTPMessageDelegateon_close(server_conn)在连接关闭时调用HTTPMessageDelegate消息处理回调接口包含四个钩子headers_received(start_line, headers)头部解析完成时调用可返回Future以延迟读取正文data_received(chunk)收到一块正文时调用可返回Future做流控finish()最后一块数据接收完毕后调用on_connection_close()连接未完成请求即关闭时调用与finish二选一HTTPConnection应用侧写响应的接口含write_headers(start_line, headers, chunkNone)头部可与首块正文同写start_line.version字段被忽略、write(chunk)、finish()三个方法均返回 Future 用于流控6.0 起移除了callback参数。tornado/test/http1connection_test.py中的HTTPMessageDelegate导入即是对该接口的直接使用验证。URL 工具url_concat 与 split_host_and_porturl_concat向已有查询串追加参数url_concat(url, args)无论 URL 是否已有查询参数都能正确拼接。args可以是字典或键值对列表/元组——后者允许同一键出现多个值 from tornado.httputil import url_concat url_concat(http://example.com/foo, dict(cd)) http://example.com/foo?cd url_concat(http://example.com/foo?ab, dict(cd)) http://example.com/foo?abcd url_concat(http://example.com/foo?ab, [(c, d), (c, d2)]) http://example.com/foo?abcdcd2实现上先用urlparse拆解 URL用parse_qsl(keep_blank_valuesTrue)保留已有查询参数含空值追加新参数后用urlencode重新编码并用urlunparse重组。args为None时原样返回 URL传入其他类型会抛出TypeError。边界行为均有测试佐证见 tornado/test/httputil_test.py尾随?url_concat(http://localhost/path?, ...)→ 正常拼出?yyzz无值的孤参数?x被保留为x空值片段fragmenturl_concat(http://localhost/path#tab, [(y,y)])→?yy#tab片段保持在末尾参数值中的特殊字符会被百分号编码(y, /y)→y%2Fy重复键保持顺序?r1r2yy。tornado/auth.py中from tornado.httputil import url_concat的使用表明这是 OAuth/OpenID 等授权流程中拼接带redirect_uri、state等参数的授权 URL 的标准工具。split_host_and_port拆分 host:port from tornado.httputil import split_host_and_port split_host_and_port(example.com:8080) (example.com, 8080) split_host_and_port(example.com) (example.com, None)基于_netloc_re re.compile(r^(.):(\d)$)无端口时返回(host, None)。HTTPServerRequest.__init__中self.host_name split_host_and_port(self.host.lower())[0]正是用它从 Host 头提取纯主机名。qs_to_qslparse_qs 结果的还原qs_to_qsl(qs)是一个生成器把urllib.parse.parse_qs产生的{name: [values]}结构还原为键值对序列用于在两种表示之间互转import urllib.parse from tornado.httputil import qs_to_qsl qs urllib.parse.parse_qs(a1b2a3) list(qs_to_qsl(qs)) # 包含 (a,1)、(a,3)、(b,2)表单与文件上传解析parse_body_arguments 体系统一入口 parse_body_argumentsparse_body_arguments(content_type, body, arguments, files, headersNone, *, configNone)同时支持两种表单格式arguments普通字段和files上传文件两个字典会被就地更新application/x-www-form-urlencoded用parse_qs_bytes(keep_blank_valuesTrue)解析字段数受config.urlencoded.max_arguments限制multipart/form-data要求content_type必须含boundary参数否则抛出HTTPInputError。两种格式下若请求头存在Content-Encoding都会抛出HTTPInputError(Unsupported Content-Encoding: ...)——这防止了压缩体绕过长度与内容校验。测试用例演示了典型用法data ba1b2a3 args, files {}, {} parse_body_arguments(application/x-www-form-urlencoded, data, args, files) assert args[a] [b1, b3] assert args[b] [b2] assert files {}multipart 解析与 HTTPFileparse_multipart_form_data(boundary, data, arguments, files, *, configNone)解析原始 multipart 字节流支持带引号的 boundary如 Google App Engine XMPP 场景并要求存在最终边界--boundary--。每个 part 的头部用HTTPHeaders.parse(..., _chars_are_bytesFalse)解析允许非 ASCII 文件名Content-Disposition必须是form-data且必须含name参数含filename的 part 进入filesContent-Type缺省为application/unknown否则进入arguments。解析出的文件是HTTPFile对象——它继承ObjectDict因此属性与字典键可互换访问from tornado.httputil import HTTPFile file files[files][0] file[filename] # 等价于 file.filename file[body] # 等价于 file.body file[content_type]三个固定字段为filename、body、content_type。5.1 起支持 RFC 2231/5987 的filename*编码UTF-8%C3%A1b.txt正确解析为áb.txt普通 UTF-8 原始文件名如测试.txt也可用相关断言见测试 tornado/test/httputil_test.py。解析安全限制ParseBodyConfig 配置族6.5.5tornado.httputil的 multipart 解析历史上多次出现 DoS 漏洞因此 6.5.5 起引入了带默认限额的配置 dataclass 体系源码 tornado/httputil.py配置类字段默认值含义ParseMultipartConfigenabledTrue设为False可完全禁用 multipart 解析不处理文件上传的应用建议关闭max_parts100一个 multipart 请求允许的最大 part 数HTML 表单每个input至少对应一个 partmax_part_header_size10 * 1024每个 part 头部的最大字节数ParseUrlEncodedConfigmax_arguments1000urlencoded 请求允许的最大参数字段数ParseBodyConfigmultipart/urlencoded上述默认实例组合上述两套配置set_parse_body_config(config)设置全局默认解析配置——官方定位是 6.5.5 引入限额后的临时补救措施按连接级别的非全局配置将在未来版本提供from tornado.httputil import ( ParseBodyConfig, ParseMultipartConfig, parse_body_arguments, set_parse_body_config, HTTPInputError, ) # 禁用 multipart 解析 config ParseBodyConfig(multipartParseMultipartConfig(enabledFalse)) set_parse_body_config(config) # 此时解析 multipart 体将报错 content_type multipart/form-data; boundaryfoo multipart_body b--foo--\r\n try: parse_body_arguments(content_type, multipart_body, {}, {}) except HTTPInputError as e: print(e) # ...: multipart/form-data parsing is disabled # 恢复默认 set_parse_body_config(ParseBodyConfig())源码中的限制触发点清晰可循parse_multipart_form_data用data.split(b-- boundary b\r\n, config.max_parts 1)截断拆分以检测too many parts用eoh config.max_part_header_size检测part header too largeparse_body_arguments将max_num_fieldsconfig.urlencoded.max_arguments传给parse_qs_bytes。HTTPServerRequest构造时解析查询串也复用了同一限额读取全局_DEFAULT_PARSE_BODY_CONFIG.urlencoded.max_arguments源码注释说明这是因为请求对象无法访问按连接配置。测试test_max_arguments用 1001 个字段验证了触发HTTPInputError(Max number of fields exceeded)测试test_multipart_config则逐一验证了max_parts0、max_part_header_size10、enabledFalse三种限制。另外_parse_header/_parseparam沿用了 Python 2.7cgi.py的算法并做了两处关键修正正确处理引号内分号组合、支持无值参数WebSocket 扩展协商场景与 RFC 2231/5987 非 ASCII 值且 6.4 后引入了 cpython PR 136072 的线性解析逻辑避免引号字符串内分号导致二次方复杂度——测试test_disposition_param_linear_performance与test_unquote_large均为相关性能回归测试。时间与起始行解析format_timestampHTTP 日期格式format_timestamp(ts)接受多种时间输入统一输出 HTTP 报文格式的日期 from tornado.httputil import format_timestamp format_timestamp(1359312200) Sun, 27 Jan 2013 18:43:20 GMT支持int/floattime.time()风格、time.gmtime返回的 time tuple、datetime.datetimenaive 视为 UTCaware 先转 UTC。内部用calendar.timegm转纪元秒后交给email.utils.formatdate(usegmtTrue)。测试 tornado/test/httputil_test.py 覆盖了全部六种输入形态并验证非 UTC 时区最终仍输出 GMT。起始行NamedTuple 与解析函数请求与响应起始行分别由两个typing.NamedTuple表示并配有两个严格校验的解析函数 from tornado.httputil import parse_request_start_line, parse_response_start_line parse_request_start_line(GET /foo HTTP/1.1) RequestStartLine(methodGET, path/foo, versionHTTP/1.1) parse_response_start_line(HTTP/1.1 200 OK) ResponseStartLine(versionHTTP/1.1, code200, reasonOK)RequestStartLine(method, path, version)/ResponseStartLine(version, code, reason)两者都用_ABNF中的正则做fullmatch严格匹配请求行必须符合method request-target HTTP/x.y对应 RFC 7230 §3.1.1非法请求行应返回 400状态行必须符合HTTP/x.y 3位数字 原因短语版本必须以HTTP/1开头否则抛出HTTPInputErrorHTTP/2 及以上不使用这两个函数注释明确指出其仅面向 HTTP 1.x。Cookie 解析parse_cookieparse_cookie(cookie)把Cookie头解析为dict[str, str]。官方文档明确它刻意不遵循任何 Cookie 相关 RFC——因为浏览器本身也不遵守如document.cookie的种种怪癖其算法与 Django 1.9.10 完全一致4.4.2 版本加入。关键行为测试 tornado/test/httputil_test.py 覆盖详尽所有分号都是分隔符即使引号内keeblerEmc2; L\Loves\; fudge\012;会被拆成多个键无等号的块按 Mozilla Bug 169091 归为无名键同名 Cookie 只保留最后一个parse_cookie(ab; hi; ac)→{a: c, h: i}键值两侧空白会被去除值中可有空格、、冒号、Unicode 字符支持\012八进制、\、\\等反斜杠转义解引算法复刻自 Python 3.13 标准库http.cookies._unquote且经 6.4.2 优化后对超长转义串保持线性性能10 万次转义在几十毫秒内完成。认证辅助encode_username_passwordencode_username_password(username, password)5.1 新增把用户名/密码对编码为 HTTP Basic Auth 所需的字节串username:passwordfrom tornado.httputil import encode_username_password encode_username_password(user, pass) # buser:pass实现中 Unicode 输入先做 NFC 归一化避免组合字符变体绕过认证再以 UTF-8 编码最后以冒号连接。实战速查在 Tornado 应用中使用这些工具虽然tornado.web.RequestHandler已封装了大部分解析逻辑但直接使用tornado.httputil的典型场景包括自定义头部解析把AsyncHTTPClient的header_callback行直接交给HTTPHeaders.parse_line6.5 起无需补 CRLF拼接带参 URLOAuth 跳转、分页链接等场景使用url_concat替代手写?/拼接避免编码错误参见 tornado/auth.py 中的用法离线解析请求体单元测试中可用parse_body_arguments/parse_multipart_form_data直接验证表单与上传解析逻辑或复用parse_cookie、format_timestamp做协议级断言服务端定制实现HTTPServerConnectionDelegateHTTPMessageDelegate可完全替换RequestHandler的消息处理流程用于 WebSocket 握手等特殊协议Tornado 自身的 tornado/websocket.py 正是基于这一接口体系构建的相关接口定义见 tornado/httputil.py安全加固通过set_parse_body_config按需收紧max_parts、max_part_header_size、max_arguments或直接禁用 multipart防御畸形上传体的 DoS 攻击。版本演进要点4.0HTTPServerRequest由tornado.httpserver迁入tornado.httputil新增HTTPInputError、HTTPOutputError及三个协议接口4.1新增split_host_and_port5.1头部解析统一抛HTTPInputError支持 RFC 2231/5987 非 ASCII 文件名新增encode_username_password6.0HTTPConnection各写方法移除callback参数改为纯 Future 流控6.5parse_line支持带/不带 CRLF 的行头部校验采用 RFC 9110/9112 ABNF 严格模式multipart 解析引入ParseMultipartConfig限额6.5.5urlencoded 限额随ParseUrlEncodedConfig跟进6.5.8host构造参数弃用6.5.27.0预告行折叠与 LF 行终止符将移除start_line参数变为必填。延伸阅读模块 API 参考docs/httputil.rst源码实现tornado/httputil.py专门测试tornado/test/httputil_test.py服务端集成tornado/httpserver.py、docs/httpserver.rstWeb 层消费tornado/web.py、docs/web.rstRequestHandler.request即HTTPServerRequest路由层使用tornado/routing.py、docs/routing.rst客户端侧配套tornado/httpclient.py、docs/httpclient.rst【免费下载链接】tornadoTornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed.项目地址: https://gitcode.com/gh_mirrors/to/tornado创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考