PAYATHON 2026

BaseHTTPRequestHandler 如何发送 HTTP headers 和 JSON 响应体?

技术老齐

这段代码的执行顺序是:先把状态行和响应头存入内部缓冲区,再调用 end_headers() 写出它们,最后通过 wfile.write() 写出响应体。

send_response() 和 send_header() 通常不会立即向连接写入数据。end_headers() 会补上标记响应头结束的空行,并将缓冲内容写入输出流。JSON 则要先序列化,再编码成字节,才能写入 wfile。

状态行和响应头是怎样发送的?

在 Python 3.10 的 BaseHTTPRequestHandler 实现中,这几个方法各自负责以下操作:

调用执行的操作
send_response(200)缓存状态行,添加默认的 Server、Date 响应头,并记录请求日志
send_header(name, value)将一个响应头追加到内部缓冲区
end_headers()追加空行,调用 flush_headers() 写出缓冲内容
wfile.write(response_body)向输出流写入响应体字节

内部缓冲区名为 _headers_buffer,属于实现细节,正常使用时不需要直接操作。

例如:

self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(response_body)))
self.end_headers()
self.wfile.write(response_body)

最终形成的 HTTP/1.x 响应结构是:

状态行\r\n
响应头字段\r\n
响应头字段\r\n
\r\n
响应体

这里的 \r\n 表示回车和换行两个字符。响应头后的空行标记响应体的起点,因此要在写入响应体之前调用 end_headers()。

end_headers() 只负责结束并写出响应头,不会自动发送 JSON,也不会结束整个响应。对于这里设置了 Content-Length 的普通 GET 响应,客户端通过该字段判断需要读取多少字节的响应体。

使用这几个方法时,还要注意:

  • 可以直接调用 flush_headers(),提前写出已缓存的内容。不过,它不会补上结束响应头的空行。普通 JSON 响应直接使用 end_headers() 即可。
  • 写入输出流不代表客户端已经收到数据。实际传输时机还受操作系统和网络影响,一次 write() 也不一定对应一个 TCP 数据包。

为什么 JSON 要先编码成 UTF-8?

JSON 序列化和字符编码是两个独立步骤:

json_text = json.dumps(body)             # Python 对象转换成 JSON 文本,类型为 str
response_body = json_text.encode("utf-8")  # 文本转换成 bytes

wfile 是二进制输出流,接受字节数据。直接写入 str 会产生 TypeError:

# 错误:json.dumps() 返回 str
self.wfile.write(json.dumps(body))

Content-Type: application/json 只告诉客户端如何解释响应体,不会替 Python 执行 JSON 序列化或字符编码。

Content-Length 也必须按编码后的字节数计算:

response_body = json.dumps(body, ensure_ascii=False).encode("utf-8")
self.send_header("Content-Length", str(len(response_body)))

JSON 字符串的字符数不能代替字节数。例如:

text = "你好"

len(text)                  # 2 个字符
len(text.encode("utf-8"))   # 6 个字节

默认的 json.dumps() 会将中文等非 ASCII 字符转义为 \uXXXX。设置 ensure_ascii=False 可以保留这些字符。两种形式都能生成合法 JSON,发送前都需要编码成字节。

一份完整的 JSON 响应示例

可以把重复的响应逻辑提取成一个方法,让 200 和 404 响应使用相同的发送顺序:

from http.server import BaseHTTPRequestHandler, HTTPServer
import json


class Handler(BaseHTTPRequestHandler):
    def send_json(self, status_code, body):
        response_body = json.dumps(
            body,
            ensure_ascii=False,
        ).encode("utf-8")

        self.send_response(status_code)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(response_body)))
        self.end_headers()

        self.wfile.write(response_body)

    def do_GET(self):
        if self.path == "/":
            self.send_json(
                200,
                {"message": "Hello bro, this is your HTTP response"},
            )
        else:
            self.send_json(404, {"error": "Route not found"})


if __name__ == "__main__":
    server = HTTPServer(("localhost", 8000), Handler)
    print("server running on localhost 8000")
    server.serve_forever()

将代码保存为 server.py 后运行:

python server.py

查看响应头和响应体:

curl -i http://localhost:8000/
curl -i http://localhost:8000/missing

也可以使用 curl -v,查看请求信息和连接细节。

代码先完成 JSON 序列化,再构造响应。如果序列化失败,就还有机会在发送响应头之前处理错误。响应头一旦写出,已经发送的状态码就无法撤回。

为什么请求是 HTTP/1.1,响应却是 HTTP/1.0?

BaseHTTPRequestHandler 默认使用:

protocol_version = "HTTP/1.0"

它根据这个属性生成响应状态行,不会直接沿用请求中的 HTTP 版本。

请求版本和响应版本分别对应这两个属性:

self.request_version   # 客户端请求中的版本,例如 "HTTP/1.1"
self.protocol_version  # 处理器用于响应的版本,默认 "HTTP/1.0"

因此,收到 GET / HTTP/1.1 后返回 HTTP/1.0 200 OK,符合这里的默认配置。

要使用 HTTP/1.1 响应,可以在类中设置:

class Handler(BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"

使用 HTTP/1.1 后,需要注意响应体的边界。连接可能保持打开,客户端不能总靠连接关闭来判断响应结束。对于这个固定长度的 JSON 响应,仍应发送准确的 Content-Length。改成 HTTP/1.1 不会让处理器自动为响应体添加 chunked 编码。

FastAPI 的响应流程是否类似?

概念上类似:应用确定状态码、响应头和响应体,再由服务器发送给客户端。

FastAPI 通常通过 Starlette 的响应对象与 ASGI 服务器协作。例如:

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def root():
    return {"message": "Hello bro, this is your HTTP response"}

对于这种普通 JSON 响应,框架会序列化返回值,并准备响应头和响应体字节。ASGI 层使用的消息主要包括:

http.response.start:状态码和响应头
http.response.body:响应体字节及是否还有后续内容

随后,Uvicorn 等 ASGI 服务器完成协议编码和网络写入。发送 ASGI 消息的时刻,也不一定就是数据出现在网络上的时刻。

FastAPI 内部通常不会调用 BaseHTTPRequestHandler.send_response()。两者都先确定响应元信息,再发送响应体,使用的接口和服务器实现则不同。

使用时还要留意什么?

  • send_response() 会自动添加 Server 和 Date。问题中展示的响应头省略了 Date,头字段顺序也与默认实现不同。
  • 不要通过反复调用 send_response() 来修改状态码。在这里,每次调用都会追加状态行,无法替换先前缓存的状态码。
  • 响应头结束后,再调用 send_header() 无法修改已经写出的头字段。
  • 上述写入响应体的示例适用于普通 GET 响应。HEAD 响应以及 204、304 等状态有专门的响应体规则,不能直接套用。

备注:内容仅供参考。