重试 GET 几乎无害,重试 POST 却可能重复下单。这篇用一个会“创建订单后丢掉回应”的本机服务器,把 urllib3 的 Retry 对不同方法的默认行为、POST 重试的后果、以及 Idempotency-Key 的作用都量一遍。
Retry.DEFAULT_ALLOWED_METHODS = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'PUT', 'TRACE'] Retry(total=3, status_forcelist=[503]) 对 GET → 状态码 503 时是否重试:True Retry(total=3, status_forcelist=[503]) 对 HEAD → 状态码 503 时是否重试:True Retry(total=3, status_forcelist=[503]) 对 PUT → 状态码 503 时是否重试:True Retry(total=3, status_forcelist=[503]) 对 DELETE → 状态码 503 时是否重试:True Retry(total=3, status_forcelist=[503]) 对 OPTIONS→ 状态码 503 时是否重试:True Retry(total=3, status_forcelist=[503]) 对 TRACE → 状态码 503 时是否重试:True Retry(total=3, status_forcelist=[503]) 对 POST → 状态码 503 时是否重试:False Retry(total=3, status_forcelist=[503]) 对 PATCH → 状态码 503 时是否重试:False allowed_methods=None(对所有方法都重试):POST → True
Retry.DEFAULT_ALLOWED_METHODS 只有 DELETE、GET、HEAD、OPTIONS、PUT、TRACE(按 HTTP 语义是幂等的)。POST 和 PATCH 默认不会因为状态码或错误被重试。
默认 Retry(total=3)(POST 不在 allowed_methods 里):客户端结果 ConnectionError;服务器收到 1 次 POST,实际创建了 1 个订单 Retry(total=3, allowed_methods=None)(对所有方法重试):客户端结果 200(订单号 2);服务器收到 2 次 POST,实际创建了 2 个订单 allowed_methods=None + 请求带 Idempotency-Key:客户端结果 200(订单号 1);服务器收到 2 次 POST,实际创建了 1 个订单
如果你把 allowed_methods=None 打开,在“订单已创建、回应丢了”时,重试会再创建一个订单(2 个)。让重复安全需要服务器配合:请求里带 Idempotency-Key,服务器按它去重(这里我写的服务器做了),重试时只剩 1 个订单。客户端单方面加头没有用。
Retry(total=3, status_forcelist=[503])(POST 默认不重试):客户端结果 503(订单号 busy);服务器收到 1 次 POST,创建了 1 个订单 allowed_methods=None, status_forcelist=[503]:客户端结果 200(订单号 2);服务器收到 2 次 POST,创建了 2 个订单 同上 + Idempotency-Key:客户端结果 200(订单号 1);服务器收到 2 次 POST,创建了 1 个订单
先创建订单再回 503,客户端看到的是失败,但副作用已经发生。重试 POST 时必须假设“上一次可能已经生效”。
PUT 被重置一次后默认 Retry 自动重试:结果 200,服务器收到 2 次,资源值 b'v=1' GET 被重置一次后默认 Retry 自动重试:结果 200,服务器收到 2 次,资源值 None
PUT 到固定资源,重试后资源值不变;GET 无副作用。这两类自动重试是安全的(前提是服务器真的按语义实现)。
Retry(total=3):结果 200,服务器收到 2 次 Retry(total=3, read=0)(读取阶段不重试):结果 ConnectionError,服务器收到 1 次 Retry(total=3, read=1):结果 200,服务器收到 2 次 Retry(total=0):结果 ConnectionError,服务器收到 1 次
Retry 除了 total,还有 connect、read、status 等分项;读取阶段出错的 GET,read=0 时不重试,默认会重试。