HTTP gzip 长度排查:字符数、原始字节和压缩字节别混着比

10-01 4阅读

下载结果有一千多个字节,响应头却写四十五,就一定是服务端报错吗?如果响应使用 gzip,而客户端已经解压,两边计算的可能不是同一层数据。排查要先确定正在观察字符、原始编码字节,还是压缩后的正文。本文在 Python 3.12.14 实跑,仅用标准库和本机回环地址,不请求外部网站。

HTTP gzip 长度排查:字符数、原始字节和压缩字节别混着比

AI生成概念图:内容经过压缩传递后重新展开,不同阶段具有不同体积。仅作概念说明,不代表实际界面或实测数据。

建立能看到压缩正文的小实验

下面启动一个临时本机服务,由系统分配空闲端口。客户端显式接受 gzip,使用 http.client 读取响应正文,再手动解压和按 UTF-8 解码。这个客户端路径让各个阶段分开可见。整段运行结束会关闭连接、停止服务并等待线程退出。

import gzip
import threading
from http.client import HTTPConnection
from http.server import BaseHTTPRequestHandler, HTTPServer

text = "压缩测试\n" * 100
plain = text.encode("utf-8")
packed = gzip.compress(plain, mtime=0)

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.headers.get("Accept-Encoding") != "gzip":
            self.send_error(400, "This demo expects gzip")
            return
        self.send_response(200)
        self.send_header("Content-Type", "text/plain; charset=utf-8")
        self.send_header("Content-Encoding", "gzip")
        self.send_header("Content-Length", str(len(packed)))
        self.end_headers()
        self.wfile.write(packed)

    def log_message(self, *args):
        pass

server = HTTPServer(("127.0.0.1", 0), Handler)
thread = threading.Thread(target=server.serve_forever)
thread.start()
client = HTTPConnection("127.0.0.1", server.server_port, timeout=3)
try:
    client.request("GET", "/", headers={"Accept-Encoding": "gzip"})
    response = client.getresponse()
    wire = response.read()
    assert response.status == 200
    assert response.getheader("Content-Encoding") == "gzip"
    assert len(wire) == int(response.getheader("Content-Length"))
    decoded = gzip.decompress(wire)
    assert decoded == plain
    assert decoded.decode("utf-8") == text
    print("characters:", len(text))
    print("plain bytes:", len(plain))
    print("gzip bytes:", len(wire))
finally:
    client.close()
    server.shutdown()
    thread.join()
    server.server_close()

本次输出是五百个字符、一千三百个未压缩字节、四十五个 gzip 字节。中文字符经 UTF-8 编码占多个字节,因此前两个长度本来就不同。压缩后的具体体积可能随内容与实现变化,不宜把四十五当成通用比例;可靠断言是响应头长度与实际读取的编码后正文相同。

Content-Length 对应当前传递的正文

对本例这个带正文的普通成功响应,Content-Encoding 表明内容做了 gzip 编码,Content-Length 则填写编码后内容的字节数。先对正文解压,再用结果长度与该头比较,就会制造假警报。这里统计的是响应内容,不包括状态行、响应头、传输层或加密协议的额外开销。

不少客户端库和浏览器会自动解压,并保留一部分原始响应头。工具显示的大小也可能分为传输体积与资源体积。遇到不一致时先查该客户端的自动解码行为和字段定义,记录获取正文的具体接口;不能仅凭变量名叫 raw,就断定拿到的必然是线上原始字节。

解压和字符解码是两步

gzip 解压得到字节,UTF-8 解码才得到文本。顺序颠倒时,压缩字节通常无法作为正常文本解码。示例先断言解压结果与原始字节完全一致,再确认文本内容一致,这样能够区分压缩层损坏与字符集使用错误,避免把两种问题都叫“乱码”。

客户端是否接受编码由请求协商决定。示例为了聚焦长度,只接受恰好为 gzip 的请求头,并不实现权重、多种编码或其他协商规则,不能直接作为生产服务使用。真实服务如果根据请求头返回不同编码版本,还应正确安排缓存区分与表示元数据,防止把某一版本误发给另一类客户端。

验收时保留层次清楚的证据

建议记录状态码、内容编码、内容类型、响应头长度、解压前后长度,以及解码是否成功。先在固定小样本上证明每个计数口径,再处理真实下载。若真的截断,读取层或解压层通常会提供另一份异常证据;不要为了让数字一致就修改保存文件,掩盖上游问题。

也别把本例规则扩展到所有 HTTP 消息:HEAD、无正文状态、分块传输等都有各自语义,某些响应根本不提供 Content-Length。应根据请求方法、状态码与消息定界一起判断。在接收不可信压缩内容时,还要限制输入、解压后的体积和处理时间;一次性解压的小例子不适合无限大的响应。

实际排错可以先关闭自动解码,确认编码后的内容完整,再单独运行解压与字符解码。每次只改变一个处理环节,并保留原始错误。这样才能把“数字不一样”转成明确问题:计数层不同、内容被截断、编码声明错误,还是客户端重复解压。

参考资料

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。