Antes de reintentar un POST tras un fallo del proxy, establece qué puede haber hecho ya el destino. Perder la respuesta deja incierto el resultado de la operación. Una segunda petición puede crear otro trabajo aunque el primero ya exista.
En nuestra demostración local, el cliente recibió un error después de que el origen creara un trabajo. Repetir el POST creó un segundo trabajo. Repetirlo con la misma clave también creó un segundo trabajo cuando el endpoint receptor ignoró la clave. Solo el contrato de deduplicación implementado por el endpoint hizo que una repetición con la misma clave devolviera el trabajo original.
Se trata de registros de trabajo sintéticos en memoria, no de tareas de producción completadas. El resultado útil es la diferencia entre las peticiones intentadas y los efectos observados en la aplicación. Puedes reproducir ambos con la descarga.
Pierde la respuesta después de crear el trabajo
Descarga y extrae la demostración de reintentos con proxy. Desde el directorio extraído, ejecuta:
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.jsonEl ejemplo requiere Python 3.11 o posterior y fija HTTPX 0.28.1 con HTTPcore 1.0.9. Usa asyncio.timeout, añadido en Python 3.11. La instalación de dependencias usa el índice de paquetes. La demostración en sí arranca un origen HTTP temporal y un proxy de reenvío en loopback; no interviene ninguna cuenta de proveedor ni destino externo.
En cada uno de seis casos separados, el origen aplica el primer POST y envía su respuesta. El proxy lee esa respuesta completa y después cierra deliberadamente la conexión descendente sin enviar las cabeceras de respuesta. El cliente registra RemoteProtocolError en esta ejecución con versiones fijadas. Cualquier petición repetida o consulta es una acción posterior y explícita del cliente.
El orden capturado es:
origin: job applied
origin: response sent
proxy: complete origin response received
proxy: response dropped before client headers
client: request failedLos propios registros del origen produjeron esta matriz. Los recuentos de POST incluyen el intento original; una consulta es un GET aparte.
| Acción tras la respuesta perdida | POST recibidos | GET recibidos | Trabajos creados | Siguiente resultado del cliente |
|---|---|---|---|---|
| Repetir el POST sin contrato de repetición | 2 | 0 | 2 | 201, segundo trabajo |
| Reutilizar una clave que el endpoint ignora | 2 | 0 | 2 | 201, segundo trabajo |
| Reutilizar la clave soportada con la misma entrada | 2 | 0 | 1 | 201, resultado original guardado |
| Generar una clave nueva para el reintento | 2 | 0 | 2 | 201, segundo trabajo |
| Reutilizar la clave soportada con entrada modificada | 2 | 0 | 1 | 409, conflicto de entrada |
| Consultar la operación confirmada; no enviar un segundo POST | 1 | 1 | 1 | 200, trabajo existente encontrado |
Un 201 en el segundo intento significó, por tanto, dos cosas distintas en este fixture: un segundo trabajo recién creado o el resultado guardado del primer trabajo. Comprobamos las identidades de los trabajos y los recuentos de efectos en el origen para distinguirlos. La idempotencia concierne al efecto previsto de repetir una operación; el código de estado por sí solo no puede demostrarla. Semántica de idempotencia de HTTP.
El límite importante está entre aplicar la operación y entregar su respuesta. Esto es distinto de un intento de conexión que falló antes de enviar la petición de la aplicación. Para un destino HTTPS, un fallo al establecer el túnel CONNECT también ocurre en una etapa distinta de la pérdida de la respuesta a un POST enviado a través de un túnel ya establecido. La guía de tiempos de espera explica esas etapas.
El proxy de esta demostración no reintenta peticiones. Un segundo POST es una acción explícita del cliente. Esa distinción importa: la semántica HTTP prohíbe que un proxy reintente automáticamente una petición no idempotente. Un cliente también necesita una base para tratar una operación no idempotente como repetible, o evidencia de que la original no se aplicó. RFC 9110, sección 9.2.2.
Elige la siguiente acción según el estado de la operación
Una clase de excepción describe una observación del cliente. No es un recibo de la aplicación. Usa evidencia sobre la operación lógica, es decir, el trabajo que el llamador pretendía crear, para decidir qué ocurre a continuación.
| Evidencia disponible | Qué sabes | Siguiente acción |
|---|---|---|
| Un resultado autoritativo identifica el trabajo creado | La creación surtió efecto | Usa o concilia ese resultado; no vuelvas a crear el trabajo |
| El resultado es desconocido, pero el endpoint admite la repetición con tu clave de operación conservada y la misma entrada | Su contrato documentado puede permitirte repetir el intento sin repetir el efecto | Reutiliza esa identidad dentro del alcance y las reglas de retención del endpoint |
| Evidencia fiable establece que ningún intento se aplicó ni puede aplicarse todavía | La operación lógica no ha surtido efecto | Puede considerarse un nuevo intento dentro del presupuesto normal de peticiones |
| El resultado es desconocido y no existe un contrato de repetición aplicable | Repetir el POST podría crear un segundo efecto | Usa el proceso de estado/conciliación del servicio o escala la operación sin resolver |
Para el caso de consulta, el llamador conservó X-Operation-Ref: operation-1; el GET /operations/operation-1 del fixture devuelve el trabajo asociado. Esa referencia se conocía antes del POST, así que la consulta no necesita un ID de trabajo de la respuesta perdida.
Trata con cuidado una consulta vacía o fallida. Un «no encontrado» en un instante dado no establece que una petición anterior no pueda llegar o completarse más tarde. La consulta positiva de la demostración funciona porque devuelve el trabajo ya creado. No prueba la seguridad de una consulta negativa. Esta distinción se deriva del problema de la llegada tardía tratado en la AWS Builders Library.
Un retardo de backoff cambia cuándo vuelves a intentarlo. No establece si la primera operación ocurrió. Un 502, un 504, un tiempo de espera agotado o una respuesta desconectada siguen necesitando su fase de petición y su contexto de aplicación. Usa el artículo sobre códigos de estado de proxy para identificar la capa que responde; evita convertir sus categorías de error en una lista universal de reintentos de POST seguros.
Una cabecera necesita una aplicación receptora que la entienda
El fixture de clave soportada registra el trabajo y su respuesta bajo un bloqueo dentro del proceso. Dentro de su alcance de un solo llamador, la clave está ligada al endpoint del POST y se compara con la referencia de operación y la carga útil parseada. Una repetición coincidente recibe el resultado guardado del trabajo. Una entrada modificada recibe la respuesta 409 explícita del fixture sin crear otro trabajo.
El caso de la clave ignorada cambia el comportamiento de la aplicación receptora, no la escritura de la cabecera. El caso de la clave nueva cambia la identidad de operación presentada a la aplicación. Ambos crearon dos trabajos. Reutiliza la identidad de la operación prevista cuando su contrato permita una repetición; generar una clave nueva en cada intento anula esa conexión.
Este fixture conserva los registros solo en memoria durante su vida útil. Su bloqueo demuestra coordinación dentro de un único proceso. No hace duradera la creación de trabajos, no cubre un reinicio, no expira claves de forma segura ni coordina efectos externos como el envío de un mensaje. Esas propiedades requieren un diseño de aplicación que va más allá de este ejemplo.
Las API reales definen sus propios contratos. Por ejemplo, Stripe documenta que almacena el resultado de una petición para reutilizarlo con la misma clave, que comprueba los parámetros posteriores y que trata una clave reutilizada como una petición nueva una vez eliminado su registro almacenado. Esas son las reglas de Stripe; enviar una cabecera con un nombre parecido a otro endpoint no las importa. Peticiones idempotentes de Stripe.
Antes de confiar en una clave en producción, verifica cuatro cosas con el servicio receptor:
- Identidad y alcance: ¿qué llamador, cuenta, endpoint y operación identifica la clave?
- Entrada y concurrencia: ¿cómo se manejan las entradas modificadas y los intentos solapados?
- Retención y recuperación: ¿cuánto dura la protección y qué sobrevive a un reinicio o a un fallo parcial?
- Semántica del resultado: ¿qué se devuelve para una operación existente, pendiente, fallida o completada?
La discusión de diseño de AWS conecta la identidad de la petición con el registro atómico de la mutación y describe por qué las llegadas tardías y los cambios de intención necesitan un manejo explícito. Es un contexto útil para estas preguntas, no evidencia de que toda API implemente las mismas garantías. Making retries safe with idempotent APIs.
Mantén separados los reintentos del cliente y los resultados de negocio
No uses el recuento de respuestas exitosas del cliente como recuento de trabajos. Registra la identidad de la operación lógica por separado de cada intento de transporte. Cuando se pierda la respuesta, conserva un resultado sin resolver hasta que el contrato del servicio o su estado autoritativo respalden una conclusión más firme. Así, una respuesta recuperada, un resultado repetido y un trabajo recién creado quedan distinguibles en tu investigación.
Sé preciso también con la configuración de la librería. HTTPX documenta sus reintentos de transporte integrados para errores de conexión y tiempos de espera de conexión, no para fallos arbitrarios de lectura/escritura ni códigos de estado. En la implementación fijada de HTTPX 0.28.1, la rama de proxy HTTP no pasa el ajuste retries del transporte a su pool de proxy. Este experimento usa acciones explícitas del cliente y cero reintentos por defecto; no depende de que esa opción repita un POST enviado por proxy. Documentación de transportes de HTTPX, código fuente del transporte etiquetado.
Para el monitoreo, cuenta por separado los intentos, los fallos de respuesta observados, los trabajos confirmados y las operaciones lógicas sin resolver. El artículo sobre metodología de benchmark explica por qué importa un denominador de intentos. Para una integración que cambia estado, añade el resultado de la aplicación antes de dar la recuperación por exitosa.
Descargas y límites de la prueba
El archivo contiene la demostración completa, las pruebas, las versiones fijadas, el README, el JSON registrado y su CSV derivado. También puedes inspeccionar por separado el código, las pruebas, los requisitos, el README, la salida de eventos y la matriz de resultados.
ipvolt ejecutó las comprobaciones locales el 13 de septiembre de 2026 con CPython 3.14.7 en macOS arm64 y los paquetes fijados arriba. Las comprobaciones cubren la matriz de seis casos, peticiones solapadas con la misma clave, conflictos por entrada modificada, identidades distintas, la negativa a reenviar a un destino externo y la limpieza del fixture. Los casos registrados terminaron con el cliente cerrado y sin manejadores ni escritores del fixture pendientes.
La prueba usa reenvío HTTP/1.1. No prueba CONNECT, TLS, autenticación, SOCKS, proveedores externos, almacenamiento duradero, expiración de claves ni el comportamiento de llegada tardía de un servicio remoto. No hace ninguna afirmación sobre la fiabilidad de proveedores ni sobre procesamiento exactly-once.
Si quieres enterarte cuando se abra el acceso a ipvolt, únete a la lista de acceso anticipado. Un solo correo cuando se abra el acceso. Nada más. Esta demostración no describe una API de ipvolt disponible ni una función de idempotencia de ipvolt.