先给结论:requests 的默认配置几乎全是"危险默认"——默认没有超时、默认不重试、默认每次新建连接。绝大多数线上事故不是 requests 的 bug,而是这三个默认没被覆盖。
下面 9 个坑,按线上出现频率排序,每个都给现象、根因、修法。
现象:程序卡死几小时,日志没有任何报错,线程/协程全部耗尽。
根因:requests 的 timeout 默认值是 None,即无限等待。服务端不返回,客户端就一直等着。
r = requests.get(url)
r = requests.get(url, timeout=(3.05, 10))timeout=(3.05, 10) 表示连接超时 3.05 秒、读取超时 10 秒。这两个数是不同的东西:连接超时是 TCP 握手,读取超时是"连上之后等多久收到下一个字节"。只传一个数字(timeout=5)则表示两者都用 5 秒。
现象:偶发的 502/连接重置直接抛异常,明明重试一次就好。
根因:requests 本身不做任何重试。重试要挂 urllib3 的 Retry 到 HTTPAdapter 上。
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
retry = Retry(
total=3, # 总重试次数
backoff_factor=0.5, # 退避:0.5s, 1s, 2s
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["GET", "HEAD"], # 只对幂等方法重试
)
s = requests.Session()
s.mount("https://", HTTPAdapter(max_retries=retry))注意 allowed_methods:默认只重试幂等方法。如果你的重试列表里放了 POST,重复下单这类非幂等请求会出大事故。
verify=False 一关了之现象:SSLError: certificate verify failed,加 verify=False 后 warning 刷屏。
根因:证书链校验失败(内网自签证书、系统 CA 过旧、代理中间人证书)。verify=False 只是关掉校验,等于放弃了中间人攻击防护,urllib3 会持续打 InsecureRequestWarning。
修法(按优先级):
certifi 的最新 CA:requests.get(url, verify=certifi.where())verify="/path/to/ca.pem"verify=False,并配 urllib3.disable_warnings()requests.get,连接池白建现象:QPS 上不去,TIME_WAIT 堆积。
根因:requests.get() 每次都新建一个 Session,握手、DNS、TLS 全部重来。
for u in urls:
requests.get(u)
with requests.Session() as s:
for u in urls:
s.get(u, timeout=(3, 10))现象:多线程下偶发莫名其妙的连接错误、响应串号。
根因:Session 不是线程安全的。它内部的连接池虽然有一定并发能力,但 cookies、适配器状态并非为并发设计。
修法:每个线程一个 Session,或用 threading.local() 存,或改用 httpx(明确区分 Client 的线程/异步模型)。
现象:response.text 出现 æ\u0088\ue105 之类乱码。
根因:服务器返回的 Content-Type 没带 charset 时,requests 按 HTTP 默认 ISO-8859-1 猜,text 就按错编码解。
r = requests.get(url)
print(r.encoding, r.apparent_encoding) # ISO-8859-1 vs utf-8
r.encoding = r.apparent_encoding # 或 r.encoding = "utf-8"
print(r.text)判据:r.encoding 取自响应头,r.apparent_encoding 是 chardet/charset_normalizer 实际探测的结果。两者不一致时,以探测结果为准。
data= 和 json= 分不清现象:后端收不到参数,或收到的是一坨字符串。
写法 | Content-Type | 请求体 |
|---|---|---|
|
|
|
|
|
|
| 仍是 form 表单(除非手动加头) | 字符串被当表单值 |
想要 JSON 就直接用 json= 参数;手工 data=json.dumps(...) 必须自己补 headers={"Content-Type": "application/json"}。
现象:下载几百 MB 文件,内存飙到几 GB。
根因:response.content 会一次性把整个响应体读进内存。
with requests.get(url, stream=True, timeout=(3, 30)) as r:
r.raise_for_status()
with open("big.zip", "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)stream=True 之后必须消费或关闭响应,否则连接不会归还连接池(见坑 9)。
现象:跑一段时间报 ConnectionError: Max retries exceeded,或 urllib3 提示连接池已满。
根因:拿到响应后只取了 status_code 没读 body,或用 stream=True 后没读完也没 close(),连接就一直占着。
r = requests.get(url, stream=True)
r.close() # 显式归还
with requests.get(url, stream=True) as r:
...现象 | 根因 | 修法 |
|---|---|---|
程序永久挂起 |
| 始终传 |
偶发 502 没人重试 | requests 不自动重试 |
|
| 证书链校验失败 |
|
QPS 上不去 | 每次新建连接 | 复用 |
多线程偶发串号 | Session 非线程安全 | 每线程一个 Session |
中文乱码 | 默认 | 设 |
后端收不到 JSON |
| 用 |
下载大文件 OOM |
|
|
连接池耗尽 | 响应未关闭 |
|
requests 的三个危险默认:无超时、不重试、不复用连接。 把它们覆盖掉,能消掉线上八成以上的 HTTP 相关故障。再配一条:所有响应都用 with 包住,别让连接泄漏。
你在 requests 上踩过最坑的一次是什么?评论区说说。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。