HTTP 请求与响应

本节目标

你将学会拆解一条 HTTP API 调用,理解方法、URL、headers、body、状态码和响应体分别承担什么职责,并能用 Python 发出最小请求。

先建立直觉:一次有格式的问答

把 API 想成服务窗口:URL 是窗口地址,HTTP 方法说明意图,headers 是信封外的说明,body 是提交的材料;响应状态码先告诉你结果类别,响应 headers 提供元信息,响应 body 承载数据或错误细节。

HTTP 是无状态的请求/响应协议。这里的“无状态”不是服务永远不保存数据,而是每个请求的语义应能独立理解;登录会话、数据库记录等状态可由应用另行管理。

客户端 -- request --> 服务端
客户端 <-- response -- 服务端

请求的五个部分

假设要读取第 2 页任务:

GET /v1/tasks?cursor=page-2&limit=20 HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer <token>

1. 方法(method)

方法常见意图安全方法RFC 语义上幂等
GET读取资源是是
HEAD只读取响应元信息是是
POST创建资源或触发动作否否
PUT用给定表示整体创建/替换资源否是
PATCH局部修改否不一定
DELETE删除资源否是

“安全”表示客户端没有请求改变服务端状态,不代表没有日志或计费。“幂等”表示重复相同请求的预期效果等同于执行一次,不保证每次响应完全相同。不要仅凭方法名推断业务实现,仍要阅读 API 文档。

2. URL

https://api.example.com/v1/tasks?cursor=page-2&limit=20
\___/   \_____________/\_______/ \____________________/
scheme       host         path            query
  • scheme:生产 API 通常应使用 https;
  • host:服务所在主机;
  • path:资源或操作路径;
  • query:筛选、排序、游标和页大小等参数。

Python 中用 params= 传 query,不手工拼 ? 和 &:

import requests  # 导入 Requests 库,用它安全编码 query 参数并发送 HTTP 请求。
 
response = requests.get(  # 发起 GET 请求;示例地址只用于说明参数编码,不应直接当生产端点。
    "https://example.com/v1/tasks",  # 基础 URL 不手工拼接 query string。
    params={"cursor": "page-2", "limit": 20},  # 让库负责把游标和页大小正确 URL 编码。
    timeout=(3.05, 15),  # 分别限制连接与读取等待时间,避免无限阻塞。
)
print(response.request.url)  # 打印实际发送的 URL,观察 params 如何变成 query 参数。

3. Headers

headers 是不直接属于业务正文的元数据。字段名大小写不敏感。常见字段:

Header作用
Accept: application/json客户端希望收到 JSON
Content-Type: application/json当前发送的 body 是 JSON
Authorization: Bearer ...携带访问凭据
User-Agent标识客户端程序与版本
Idempotency-Key某些 API 用它识别重复创建请求;是否支持由 API 文档决定
Retry-After服务端建议多久后再试
X-Request-ID 或厂商等价字段关联一次调用的诊断标识;名称不是统一标准

Accept 描述“想收什么”,Content-Type 描述“正在发什么”,二者不要混淆。

4. Body

GET 通常用 query 参数;创建或执行操作常用 JSON body:

payload = {"title": "学习 API", "priority": 2}  # 用 Python 字典表示将被 JSON 序列化的创建请求体。
 
response = requests.post(  # 发起创建操作;生产代码还需按契约决定是否使用幂等键。
    "https://example.com/v1/tasks",  # 写入目标资源的示例地址。
    json=payload,  # Requests 负责 JSON 编码并设置合适的 Content-Type。
    timeout=(3.05, 30),  # 写入操作通常读取时间更长,但仍必须设置上限。
)

Requests 的 json= 会序列化 Python 对象并设置 JSON content type;data= 常用于表单或已编码数据。JSON 只支持有限的数据类型,datetime、Path 等对象要先转换。

5. API 契约(contract)

契约是调用方和服务方共同遵守的约定:路径、参数类型、必填字段、认证、响应 schema、错误格式、限流和版本策略。能发出 HTTP 请求不等于符合契约。例如服务端可能要求 limit 在 1~100 之间,或要求 model 只能取项目可访问的值。

响应的三个部分

HTTP/1.1 200 OK
Content-Type: application/json
X-Request-ID: req_123
 
{"items": [{"id": "t1", "title": "学习 API"}], "next_cursor": null}

状态码

先看百位分类,再看具体语义:

  • 1xx:临时信息;
  • 2xx:请求已按相应语义成功处理;
  • 3xx:重定向或缓存相关;
  • 4xx:客户端请求、认证、权限或业务条件不满足;
  • 5xx:服务端未能完成有效请求。

状态码是程序分支的重要依据,但不足以表达全部业务细节;错误 body 可能包含机器可读错误码、字段位置和 request ID。

Headers 与 body

media_type = (  # 从 Content-Type 中取出不含 charset 等参数的主 media type。
    response.headers.get("Content-Type", "")  # header 缺失时先使用空字符串,而不是抛 KeyError。
    .split(";", 1)[0]  # 将 `application/json; charset=utf-8` 截成 `application/json`。
    .strip()  # 删除服务端 header 两端可能存在的空白。
    .lower()  # 统一大小写,便于与标准 media type 比较。
)
request_id = response.headers.get("X-Request-ID")  # 保存可安全记录的服务端诊断 ID,不记录凭据。
 
response.raise_for_status()  # 先把 4xx/5xx 转成异常,避免把错误页误当成成功数据。
if media_type != "application/json" and not media_type.endswith("+json"):  # 同时接受供应商定义的 `application/*+json` 类型。
    raise ValueError("预期 JSON,实际收到其他内容")  # 不对 HTML 错误页等非 JSON 内容直接调用 .json()。
data = response.json()  # 只在 HTTP 成功且 media type 合理后解析 JSON;还应继续校验 schema。

response.json() 成功只证明 body 能解析成 JSON,不证明 HTTP 或业务成功;因此应先按状态码处理,再校验 schema。反过来,有些 204 响应没有 body,不能无条件调用 .json()。

从文档还原一次调用

阅读任何 API 文档时按此清单记录:

  1. base URL 与版本;
  2. method 与 path;
  3. 认证方式及 scope;
  4. path/query/header/body 参数,哪些必填;
  5. 成功状态码与响应 schema;
  6. 错误状态码与错误 body;
  7. 超时、分页、限流、重试和幂等约定;
  8. 是否有 SDK,SDK 是否隐藏了上述细节。

常见错误

  • 把 token 写进 URL:URL 可能进入浏览器历史、代理日志和监控系统;优先按文档放入 Authorization header。
  • 不设置超时:Requests 默认不会自动为你设置合理的请求超时。
  • 只判断 status_code == 200:创建可能返回 201,无正文成功可能返回 204。
  • 对所有响应立即 .json():网关错误可能返回 HTML,204 可能为空。
  • 手工拼 query:空格、中文、& 等需要编码,交给客户端库处理。
  • 根据 URL 中的动词猜语义:以 API 文档和 HTTP 方法为准。

练习

  1. 将一个请求拆成 method、scheme、host、path、query、headers、body。
  2. 解释 Accept 与 Content-Type 的区别。
  3. 说明为什么 DELETE 的幂等性不等于“每次都返回相同状态码”。
  4. 用 Requests 构造一个 PreparedRequest 并打印 URL 和 headers,但不要真的发送:
from requests import Request, Session  # 导入只构造请求的 Request 与负责准备请求的 Session。
 
request = Request("GET", "https://example.com/items", params={"q": "AI Agent"})  # 创建尚未发送的 GET 请求对象。
prepared = Session().prepare_request(request)  # 让 Session 应用默认 header 与 URL 参数编码,但不产生网络流量。
print(prepared.url)  # 检查最终 URL,确认空格等字符已被正确编码。

自测

  • 我能指出请求和响应各部分的职责。
  • 我能解释安全方法与幂等方法的区别。
  • 我知道 JSON 可解析不等于调用成功。
  • 我能从一页 API 文档写出调用清单。

参考资料

获取日期:2026-07-22。下一步:Requests 客户端、Session 与契约阅读。