# 代理重试：丢失一个响应，创建了两个任务

Source: https://ipvolt.com/zh/blog/proxy-retries-duplicate-jobs
Markdown: https://ipvolt.com/zh/blog/proxy-retries-duplicate-jobs.md
Language: zh-CN

[ipvolt 首页](https://ipvolt.com/zh.md) / [博客](https://ipvolt.com/zh/blog.md) / 代理重试：丢失一个响应，创建了两个任务

分析
发布于: 2026-09-13
更新于: 2026-09-13
作者： ipvolt
阅读约 2 分钟

本地代理在 POST 创建任务后丢弃了响应。了解重试何时会产生重复任务、幂等键何时才真正有用，以及何时应改用状态查询与对账流程。

在代理故障后重试 POST 之前，先确定目标站点可能已经做了什么。丢失响应意味着这次操作的结果不确定。即使第一个任务已经存在，第二个请求仍可能再创建一个任务。

在我们的本地演示中，客户端在源站创建任务之后收到了错误。重复发送 POST 创建了第二个任务。当接收端忽略键时，带同一个键重复发送同样创建了第二个任务。只有接收端实际实现的去重约定，才让同键重放返回原始任务。

这些是内存中的合成任务记录，而不是已完成的生产任务。有用的结果在于「尝试的请求数」与「观察到的应用效果数」之间的差异。你可以在下载包中复现这两者。

## 在创建任务之后丢失响应

下载并解压[代理重试演示](https://ipvolt.com/downloads/proxy-retry-jobs/proxy-retry-jobs.zip)。在解压后的目录中运行：

```sh
python3 -m venv .venv
. .venv/bin/activate
python -m pip install -r requirements.txt
python retry_jobs_demo.py > result.json
python retry_jobs_demo.py --csv result.json
```

该示例需要 Python 3.11 或更高版本，并固定使用 HTTPX 0.28.1 和 HTTPcore 1.0.9。它使用 [Python 3.11 新增的 `asyncio.timeout`](https://docs.python.org/3/library/asyncio-task.html#asyncio.timeout)。安装依赖会访问包索引。演示本身在回环地址上启动一个临时的 HTTP 源站和正向代理；不涉及任何供应商账号或外部目标站点。

在六个独立案例中的每一个里，源站都会应用第一个 POST 并发送其响应。代理读取完整的响应，然后故意关闭下游连接，不向客户端发送响应头。在这次固定版本的运行中，客户端记录到 `RemoteProtocolError`。任何重复请求或查询都是客户端随后的显式动作。

捕获到的顺序如下：

```text
origin: job applied
origin: response sent
proxy: complete origin response received
proxy: response dropped before client headers
client: request failed
```

源站自身的记录生成了下面这张矩阵。POST 计数包含最初的尝试；查询是单独的 GET。

| 响应丢失后的动作 | 收到的 POST | 收到的 GET | 创建的任务 | 客户端下一次结果 |
|---|---:|---:|---:|---|
| 在没有重放约定的情况下重复 POST | 2 | 0 | 2 | 201，第二个任务 |
| 重用一个接收端忽略的键 | 2 | 0 | 2 | 201，第二个任务 |
| 重用受支持的键且输入不变 | 2 | 0 | 1 | 201，原始保存结果 |
| 为重试生成新键 | 2 | 0 | 2 | 201，第二个任务 |
| 重用受支持的键但输入已更改 | 2 | 0 | 1 | 409，输入冲突 |
| 查询已确认的操作；不发送第二个 POST | 1 | 1 | 1 | 200，找到现有任务 |

因此在这个夹具中，第二次尝试的 201 意味着两种不同的情况：新创建的第二个任务，或第一个任务的保存结果。我们通过检查任务标识和源站的效果计数来区分它们。幂等性关注的是重复某个操作的预期效果；仅凭状态码无法证明它。[HTTP 幂等性语义](https://www.rfc-editor.org/rfc/rfc9110.html#section-9.2.2)。

重要的边界在于「应用操作」与「交付其响应」之间。这与在应用请求发出之前就失败的连接尝试不同。对于 HTTPS 目标站点，建立 CONNECT 隧道时的失败，与通过已建立隧道发送 POST 后丢失响应，也发生在不同的阶段。[超时指南](/zh/guides/proxy-timeout-troubleshooting)解释了这些阶段。

本演示中的代理不会重试请求。第二个 POST 是客户端的显式动作。这一区别很重要：HTTP 语义禁止代理自动重试非幂等请求。客户端也需要有把非幂等操作视为可重复的依据，或者有原始请求未被应用的证据。[RFC 9110 第 9.2.2 节](https://www.rfc-editor.org/rfc/rfc9110.html#section-9.2.2)。

## 根据操作的状态选择下一步动作

异常类描述的是客户端的观察结果。它不是应用层的回执。请依据关于逻辑操作（即调用方想要创建的那个任务）的证据来决定接下来做什么。

| 可用的证据 | 你知道什么 | 下一步动作 |
|---|---|---|
| 权威结果标识出了已创建的任务 | 创建已生效 | 使用或对账该结果；不要再次创建任务 |
| 结果未知，但接收端支持用你保留的操作键和不变的输入进行重放 | 其文档化的约定可能允许你重复尝试而不重复效果 | 在接收端的作用域和保留规则内重用该标识 |
| 可靠证据表明没有任何尝试被应用，也不可能再被应用 | 逻辑操作尚未生效 | 可以在正常的请求预算内考虑新的尝试 |
| 结果未知，且不存在适用的重放约定 | 重复 POST 可能产生第二次效果 | 使用该服务的状态/对账流程，或将未解决的操作上报 |

对于查询案例，调用方保留了 `X-Operation-Ref: operation-1`；夹具的 `GET /operations/operation-1` 返回关联的任务。该引用在 POST 之前就已知，因此查询不需要来自丢失响应的任务 ID。

要谨慎对待空结果或失败的查询。某一时刻的「未找到」并不能证明更早的请求不会在之后到达或完成。演示中的正向查询之所以有效，是因为它返回了已经创建的任务。它没有测试负向查询的安全性。这一区别源自 [AWS Builders Library](https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/) 中讨论的迟到请求问题。

退避延迟改变的是你再次尝试的时间。它无法确定第一次操作是否已经发生。502、504、超时或断开的响应仍然需要结合其请求阶段和应用上下文来判断。请使用[代理状态码文章](/zh/blog/proxy-status-codes-407-429-502)来识别响应来自哪一层；不要把其中的错误分类变成一份通用的「可安全重试 POST」清单。

## 请求头需要一个能理解它的接收应用

受支持键的夹具在进程内锁的保护下记录任务及其响应。在其单调用方的作用域内，键绑定到 POST 端点，并与操作引用和解析后的载荷进行比较。匹配的重复请求会收到保存的任务结果。输入已更改的请求会收到夹具明确的 409 响应，而不会创建另一个任务。

忽略键的案例改变的是接收应用的行为，而不是请求头的写法。新键的案例改变的是呈现给应用的操作标识。两者都创建了两个任务。当约定允许重放时，请重用预期操作的标识；每次尝试都生成新键会破坏这种关联。

该夹具只在其生命周期内将记录保存在内存中。它的锁演示的是单个进程内的协调。它没有让任务创建持久化、没有覆盖重启、没有安全地让键过期，也没有协调发送消息之类的外部效果。这些特性需要超出本示例的应用设计。

真实的 API 会定义自己的约定。例如，Stripe 的文档说明会存储请求结果以便用同一个键重用、会检查后续参数，并在存储的记录被移除后把重用的键当作新请求处理。这些是 Stripe 的规则；向另一个端点发送名称相似的请求头并不会带来这些规则。[Stripe 幂等请求](https://docs.stripe.com/api/idempotent_requests)。

在生产环境依赖某个键之前，请与接收服务确认四件事：

- **标识与作用域：**该键标识的是哪个调用方、账号、端点和操作？
- **输入与并发：**输入更改和重叠的尝试如何处理？
- **保留与恢复：**保护持续多久，重启或部分故障后哪些内容得以保留？
- **结果语义：**对于已存在、进行中、失败或已完成的操作会返回什么？

AWS 的设计讨论把请求标识与变更的原子记录联系起来，并说明了为何迟到请求和意图更改需要显式处理。它是这些问题的有用背景，而不是每个 API 都实现相同保证的证据。[用幂等 API 让重试更安全](https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/)。

## 将客户端重试与业务结果分开

不要把客户端的成功响应计数当作任务数。将逻辑操作标识与每次传输尝试分开记录。当响应丢失时，在服务的约定或权威状态支持更强的结论之前，保留「结果未解决」的状态。这样在你的调查中，恢复的响应、重放的结果和新创建的任务就能相互区分。

对库的设置也要精确。HTTPX 的文档说明其内置传输重试只针对连接错误和连接超时，而不是任意的读/写失败或状态码。在固定版本的 HTTPX 0.28.1 实现中，HTTP 代理分支不会把传输层的 `retries` 设置传入其代理连接池。本实验使用显式的客户端动作和默认为零的重试；它不依赖该选项来重放经代理的 POST。[HTTPX 传输文档](https://www.python-httpx.org/advanced/transports/#http-transport)、[标记版本的传输源码](https://raw.githubusercontent.com/encode/httpx/0.28.1/httpx/_transports/default.py)。

在监控方面，请分别统计尝试次数、观察到的响应失败、已确认的任务和未解决的逻辑操作。[基准测试方法文章](/zh/blog/what-a-proxy-benchmark-should-measure)解释了为什么以尝试次数作分母很重要。对于会改变状态的集成，在宣布恢复成功之前要加入应用层的结果。

## 下载与测试边界

[压缩包](https://ipvolt.com/downloads/proxy-retry-jobs/proxy-retry-jobs.zip)包含完整的演示、测试、版本固定、README、记录的 JSON 及其派生的 CSV。你也可以分别查看[代码](https://ipvolt.com/downloads/proxy-retry-jobs/retry_jobs_demo.py)、[测试](https://ipvolt.com/downloads/proxy-retry-jobs/test_retry_jobs.py)、[依赖要求](https://ipvolt.com/downloads/proxy-retry-jobs/requirements.txt)、[README](https://ipvolt.com/downloads/proxy-retry-jobs/README.md)、[事件输出](https://ipvolt.com/downloads/proxy-retry-jobs/example-output.json)和[结果矩阵](https://ipvolt.com/downloads/proxy-retry-jobs/example-matrix.csv)。

ipvolt 于 2026 年 9 月 13 日在 macOS arm64 上使用 CPython 3.14.7 和上述固定版本的包运行了本地检查。检查涵盖六案例矩阵、重叠的同键请求、输入更改冲突、不同的标识、拒绝转发到外部目标站点以及夹具清理。记录的案例结束时客户端已关闭，没有残留的夹具处理程序或写入器。

测试使用 HTTP/1.1 转发。它没有测试 CONNECT、TLS、身份验证、SOCKS、外部供应商、持久存储、键过期或远程服务的迟到请求行为。它不对供应商可靠性或「恰好一次」处理做任何声明。

如果你想在 ipvolt 开放访问时收到通知，请[加入早期访问名单](https://ipvolt.com/#waitlist-closing)。访问开放时发送一封邮件。仅此而已。本演示并不描述任何可用的 ipvolt API 或 ipvolt 幂等功能。

## 参考来源

- [RFC 9110: idempotency and retry semantics](https://www.rfc-editor.org/rfc/rfc9110.html#section-9.2.2)
- [AWS Builders Library: making retries safe with idempotent APIs](https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/)
- [Stripe: idempotent requests](https://docs.stripe.com/api/idempotent_requests)
- [HTTPX: transport retries](https://www.python-httpx.org/advanced/transports/#http-transport)
- [HTTPX 0.28.1: transport implementation](https://raw.githubusercontent.com/encode/httpx/0.28.1/httpx/_transports/default.py)
- [Python: asyncio.timeout](https://docs.python.org/3/library/asyncio-task.html#asyncio.timeout)

## 第一时间了解 ipvolt 开放体验。

顺便一提

ipvolt 仍在开发中。留下邮箱，开放体验时我们只会通知你一次。

开放体验时仅发一封通知邮件，不发送其他邮件。

[申请抢先体验](https://ipvolt.com/zh/blog/proxy-retries-duplicate-jobs#waitlist-blog-end)

[隐私政策](https://ipvolt.com/privacy)


## 相关文章

- [MostLogin 代理设置：添加、测试与团队共享](https://ipvolt.com/zh/blog/mostlogin-proxy-setup.md) (分析, 2026年9月17日, 阅读约 2 分钟): 逐步在 MostLogin 浏览器配置文件中添加代理：协议、主机和端口、账号密码、IP 检测、团队权限、批量导入，以及最常见的出错原因。
- [Amazon 价格：你的报价与 Featured Offer](https://ipvolt.com/zh/blog/amazon-offer-vs-featured-offer.md) (分析, 2026年9月16日, 阅读约 2 分钟): 使用匹配的上下文、已知运费以及一个经过测试、能保留缺失数据的离线模型，把你的 Amazon 报价与 Featured Offer 进行比较。
- [Amazon 商品信息更新：已接受不等于已生效](https://ipvolt.com/zh/blog/amazon-listing-update-reconciliation.md) (分析, 2026年9月14日, 阅读约 2 分钟): 通过区分已提交属性、在售报价、库存与可购买状态来诊断已被接受的 Amazon 商品信息更新，并附一张实用的对账矩阵供参考。

## 相关指南

- [一次一个阶段地排查代理超时](https://ipvolt.com/zh/guides/proxy-timeout-troubleshooting.md): 用 curl 计时把代理 DNS、TCP、CONNECT、TLS 和响应延迟分开，然后设置请求截止时间，并判断重试是否安全。
- [Python Requests 代理配置：认证与 SOCKS5](https://ipvolt.com/zh/guides/python-requests-proxy.md): 在 Python Requests 中配置代理：proxies 字典、Session 默认值、凭据、通过 requests[socks] 使用 SOCKS5、环境变量与 ProxyError。
- [在 curl 中使用代理：-x、环境变量、SOCKS5 与认证](https://ipvolt.com/zh/guides/curl-proxy-setup.md): 如何在 curl 中使用代理：-x 参数、http_proxy 与 https_proxy 变量、通过 socks5h 使用 SOCKS5、代理认证，以及如何解读 CONNECT 与 407 错误。

## 关于 ipvolt

来自 ipvolt 团队的技术分析。

ipvolt 尚未开放使用。

[阅读英文原文](https://ipvolt.com/blog/proxy-retries-duplicate-jobs.md)
