Solicitudes API condicionales que evitan las sobrescrituras de agentes
Las solicitudes API condicionales protegen las actualizaciones hechas por agentes contra sobrescrituras obsoletas. Aprende sobre ETag, campos de versión, If-Match, errores 412 y reglas seguras para reintentos.

Un agente puede generar una solicitud de API perfectamente válida y aun así causar un daño real. El fallo ocurre cuando lee un registro, otro actor lo cambia y el agente después vuelve a escribir su copia antigua sobre el estado más reciente. La autenticación no evita esto. La autorización tampoco. La solicitud procedía de una identidad permitida, pero llevaba una visión obsoleta de la realidad.
Las solicitudes API condicionales corrigen precisamente ese fallo. El cliente dice, en esencia: «aplica este cambio solo si el recurso sigue siendo la versión que observé». El servidor comprueba esa afirmación como parte de la escritura. Si es falsa, rechaza la acción antes de cambiar nada.
Este contrato importa más con agentes de programación que con una persona que pulsa botones en un formulario. Los agentes pueden leer muchos recursos, detenerse para inspeccionar código o ejecutar pruebas y después emitir un conjunto de escrituras cuando el mundo ya ha cambiado. Trata cada actualización relevante como una operación de lectura, modificación y escritura, salvo que la API pueda demostrar que se trata de una adición o de un comando conmutativo.
Las actualizaciones perdidas ocurren en flujos normales de lectura, modificación y escritura
Una actualización perdida ocurre cuando dos escritores parten del mismo estado antiguo y la escritura posterior borra la anterior. No hace falta una caída de la base de datos, un usuario malicioso ni una red defectuosa. Basta con un servidor que acepte una sustitución incondicional.
Considera una configuración de despliegue expuesta como JSON:
{
"name": "billing-worker",
"replicas": 3,
"image": "registry.example/billing:2.4.0",
"maintenanceMode": false
}
Un agente la lee para aumentar replicas de 3 a 5 antes de una prueba de carga. Mientras trabaja, un operador cambia maintenanceMode a true para investigar un problema en una cola. Si el agente envía después un PUT completo usando el documento guardado, puede devolver maintenanceMode a false. La solicitud sí cambió las réplicas como estaba previsto. También deshizo una decisión de seguridad que el agente nunca llegó a ver.
Una actualización parcial reduce el alcance del daño, pero no elimina la carrera. Si un agente envía un PATCH para sustituir /replicas, ese campo aún puede haber cambiado desde la lectura. Además, la decisión de establecer las réplicas en 5 puede depender de campos que hayan cambiado en otra parte. PATCH describe la forma del cuerpo de la solicitud. No indica de qué versión del recurso parte ese cuerpo.
Por eso, «nuestra interfaz solo cambia un campo» no es un diseño de concurrencia. Una interfaz puede ocultar el problema durante un tiempo porque las personas actúan despacio y ven páginas recientes. Un proceso autónomo no cuenta con esas protecciones accidentales.
Un ETag identifica la representación que vio el cliente
El encabezado de respuesta ETag es un validador HTTP. Cuando un servidor devuelve una representación, puede adjuntar un token que identifique esa versión de la representación:
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "deploy-8f31c2"
{
"name": "billing-worker",
"replicas": 3,
"image": "registry.example/billing:2.4.0",
"maintenanceMode": false
}
La cadena no tiene un formato interno obligatorio. Puede codificar una revisión de base de datos, un hash del contenido o un valor opaco generado. Los clientes deben tratarla como opaca. No analices una etiqueta para descubrir un número de revisión ni construyas una a partir de un cuerpo JSON. El servidor define su significado.
RFC 9110 define las etiquetas de entidad y distingue entre etiquetas fuertes y débiles. Un ETag fuerte tiene la sintaxis habitual entre comillas, como "deploy-8f31c2". Indica que las representaciones coinciden byte a byte según la semántica de representación elegida por el servidor. Una etiqueta débil comienza por W/, como W/"deploy-8f31c2", e indica únicamente que dos representaciones son suficientemente parecidas desde el punto de vista semántico para validar una caché.
Esa diferencia se confunde constantemente. Los validadores débiles sirven para muchas comprobaciones de caché en GET. Son la herramienta equivocada para proteger una escritura, porque dos representaciones «lo bastante parecidas» aún pueden diferir en un campo que la escritura destruiría. RFC 9110 exige que If-Match use una comparación fuerte. Si tu API solo publica ETag débiles, no ha proporcionado un ETag adecuado para la concurrencia optimista.
Un recurso puede tener ETag diferentes para distintas representaciones. El JSON con formato, el JSON compacto o los formatos negociados por contenido pueden recibir cada uno su propio validador. Es un comportamiento HTTP legítimo, pero incómodo para los clientes de API. Cuando sea posible, mantén una representación canónica estable en los endpoints de escritura. Así, el ETag que el cliente recibió mediante GET sigue siendo válido para PUT, PATCH y DELETE.
If-Match convierte una comprobación de versión en una obligación del servidor
If-Match coloca el ETag esperado en una solicitud insegura. El servidor ejecuta el método solo cuando la representación actual coincide exactamente con una de las etiquetas proporcionadas.
Un agente puede leer un registro y conservar el encabezado recibido:
curl -i \
-H 'Authorization: Bearer $TOKEN' \
https://api.example.test/v1/deployments/billing-worker
La respuesta incluye:
ETag: "deploy-8f31c2"
Después puede enviar el cambio mínimo que pretende, usando el validador de esa lectura:
curl -i -X PATCH \
-H 'Authorization: Bearer $TOKEN' \
-H 'Content-Type: application/json-patch+json' \
-H 'If-Match: "deploy-8f31c2"' \
--data '[{"op":"replace","path":"/replicas","value":5}]' \
https://api.example.test/v1/deployments/billing-worker
Si el recurso sigue en esa versión, el servidor aplica el parche y devuelve un ETag nuevo:
HTTP/1.1 200 OK
ETag: "deploy-a19d77"
Content-Type: application/json
{
"name": "billing-worker",
"replicas": 5,
"image": "registry.example/billing:2.4.0",
"maintenanceMode": false
}
Si la edición del operador cambió antes la versión actual, el servidor devuelve:
HTTP/1.1 412 Precondition Failed
Content-Type: application/problem+json
{
"type": "https://api.example.test/problems/precondition-failed",
"title": "The deployment changed after it was read",
"status": 412,
"detail": "Fetch the current deployment before retrying this update."
}
RFC 9110 establece que un servidor de origen no debe ejecutar el método solicitado cuando una condición If-Match resulta falsa. Esa es la propiedad que estás obteniendo. La comprobación debe ocurrir en la misma operación atómica que la mutación. Un controlador que lee la fila, compara una revisión en la memoria de la aplicación y escribe después sigue teniendo una carrera entre la comparación y la escritura.
En una base de datos relacional, la implementación suele parecerse a una actualización condicional:
UPDATE deployments
SET replicas = :replicas,
revision = revision + 1
WHERE id = :id
AND revision = :expected_revision;
Si el número de filas afectadas es cero, la API devuelve 412. Si es uno, devuelve el documento actualizado y obtiene su siguiente ETag a partir de la nueva revisión. Coloca la comprobación en la cláusula WHERE o usa una primitiva transaccional equivalente de comparación e intercambio. No la dividas en dos consultas separadas y la consideres segura.
Los campos de versión exponen el mismo contrato en los datos de la aplicación
Un campo de versión es un validador a nivel de aplicación. Ofrece a los clientes una revisión visible que devuelven en el cuerpo de la solicitud, en la consulta o en un encabezado específico. Puede resultar más fácil de manejar cuando los clientes usan SDK generados, colas de mensajes o protocolos que no conservan bien los encabezados de respuesta HTTP.
Un GET podría devolver:
{
"id": "billing-worker",
"revision": 42,
"replicas": 3,
"maintenanceMode": false
}
La actualización puede expresar su expectativa de forma explícita:
PATCH /v1/deployments/billing-worker HTTP/1.1
Content-Type: application/json
{
"expectedRevision": 42,
"replicas": 5
}
El servidor compara expectedRevision con la revisión almacenada de forma atómica. Si tiene éxito, incrementa la revisión. Si no coincide, rechaza la solicitud con una respuesta documentada, normalmente 412 cuando el campo actúa como precondición.
No confundas un campo de versión con una marca de tiempo. Una revisión entera monotónica hace clara la igualdad. Las marcas de tiempo plantean preguntas incómodas: ¿qué precisión almacena el servidor?, ¿pueden dos escrituras caer en el mismo intervalo de precisión?, ¿cambió el valor durante la serialización?, ¿una réplica asigna la hora de otra manera? Algunos problemas tienen solución, pero un contador de revisiones requiere menos explicaciones.
Los ETag y los campos de versión no compiten entre sí. Una API puede exponer ambos: el ETag aporta semántica HTTP estándar y la revisión ayuda al código de la aplicación a mostrar o reconciliar cambios. Ambos deben proceder del mismo estado confirmado. Si uno indica la versión 42 y el otro se refiere accidentalmente a la 41, los clientes no tienen una forma fiable de recuperarse.
Evita aceptar un ETag o un campo de revisión si pueden no coincidir. Elige una precondición autorizada para una ruta o exige que ambas coincidan. Los contratos de entrada flexibles parecen amables hasta que un cliente envía una revisión obsoleta en el cuerpo junto con un encabezado actualizado copiado de otra parte y nadie sabe qué afirmación respetó el servidor.
If-None-Match protege la creación, no la sustitución obsoleta
If-None-Match invierte el predicado. Indica que el método solo puede continuar si la representación actual no coincide con ninguna de las etiquetas proporcionadas. En métodos inseguros, una condición falsa produce 412.
Su forma de mutación más útil es If-None-Match: *, que significa «crea esto solo si no existe ninguna representación actual». Un cliente puede intentar de forma segura crear un recurso con nombre:
curl -i -X PUT \
-H 'Authorization: Bearer $TOKEN' \
-H 'Content-Type: application/json' \
-H 'If-None-Match: *' \
--data '{"name":"nightly-export","schedule":"0 2 * * *"}' \
https://api.example.test/v1/jobs/nightly-export
Si otro cliente ya creó ese trabajo, el servidor rechaza la solicitud en lugar de sustituirlo silenciosamente. Esto resulta útil cuando un agente ha derivado un identificador y no debe apropiarse de un objeto existente con el mismo nombre.
No envíes If-Match: * para la concurrencia optimista normal. Solo exige que exista alguna representación actual. Da al agente permiso para sobrescribir cualquier versión actual, incluida una que nunca leyó. Eso protege la existencia, no contra las actualizaciones perdidas.
En GET y HEAD, If-None-Match permite usar caché. Una etiqueta coincidente normalmente produce 304 Not Modified, sin cuerpo de respuesta. Ese comportamiento de caché suele ser el primer motivo por el que los desarrolladores conocen los ETag. No dejes que eso te lleve a tratar los validadores como un mecanismo exclusivo de caché. El mismo mecanismo tiene consecuencias mucho más importantes en las escrituras.
Una respuesta a una escritura obsoleta necesita una política disciplinada del agente
Un 412 debe detener el plan de mutación actual. La premisa antigua del agente ha dejado de ser válida y enviar de nuevo la misma solicitud no la hará cierta.
La secuencia segura de recuperación es breve:
- Obtén la representación actual y su nuevo validador.
- Compara los campos o supuestos de estado que sustentan la acción prevista, no solo el campo mencionado en el parche.
- Reintenta con el nuevo validador únicamente si la intención sigue siendo correcta sin reinterpretarla.
- Pide aprobación o detente cuando el estado actual cambie el significado, el alcance o el riesgo de la acción.
El segundo punto es donde los clientes automatizados suelen hacer trampa. Supón que un agente planeaba eliminar a un usuario de un grupo de acceso después de leer una lista de miembros. Una persona cambia después el rol del usuario de contratista a responsable de respuesta ante incidentes. El agente aún puede ejecutar una eliminación sintácticamente válida tras volver a consultar el recurso. No debería hacerlo automáticamente, porque el cambio de rol vuelve cuestionable el plan original.
Mantén un registro de trabajo sencillo pero completo: URI del recurso, ETag o revisión observados, campos leídos, mutación prevista y respuesta. Un ejecutor de herramientas puede conservarlo en memoria durante una tarea breve. Un flujo autónomo más largo debería persistirlo en su propio estado de tareas auditado. Nunca le pidas al modelo que recuerde un validador solo a partir de prosa; es fácil perder, alterar o reutilizar valores de encabezado entrecomillados para el recurso equivocado.
Sallyport puede impedir que el agente acceda directamente a la credencial de la API mientras ejecuta la llamada HTTP, pero el agente aún debe conservar y enviar el ETag como datos normales de la solicitud. El aislamiento de credenciales y el control de concurrencia resuelven fallos distintos, así que usa ambos cuando la acción tenga consecuencias.
412, 409 y 428 describen fallos diferentes
Devuelve 412 Precondition Failed cuando el cliente envió un encabezado de solicitud condicional o una precondición equivalente documentada, y esa condición es falsa. La respuesta indica con precisión que el recurso ya no está en el estado que el cliente afirmó.
Devuelve 428 Precondition Required cuando el servidor exige una precondición para una ruta y el cliente la omitió. RFC 6585 define este estado específicamente para evitar actualizaciones perdidas. La respuesta puede indicar que PATCH requiere If-Match e incluir el ETag actual si mostrarlo no crea un problema de divulgación.
Devuelve 409 Conflict cuando la solicitud entra en conflicto con el estado de la aplicación incluso después de que su condición de versión haya pasado. Por ejemplo, un cliente podría enviar un valor If-Match que coincide con una factura actual, pero el servidor rechaza la cancelación porque ya ha comenzado la liquidación del pago. La comprobación de versión pasó; el comando de negocio sigue entrando en conflicto con el estado de la factura.
No agrupes estos casos en un único error genérico. Un agente debe reaccionar de forma diferente:
- Después de 428, obtiene el recurso y reintenta con la condición necesaria.
- Después de 412, vuelve a consultar el recurso y reevalúa la intención original.
- Después de 409, inspecciona el conflicto del dominio y sigue el proceso de resolución de negocio de la API.
Un cuerpo de error útil nombra el recurso, identifica la condición fallida sin repetir secretos y comunica si un GET nuevo puede ayudar. No debe fingir que un reintento es inofensivo. El estado HTTP proporciona la categoría legible por la máquina; el cuerpo ofrece al operador el contexto suficiente para decidir qué ocurre después.
Las fechas de última modificación son una alternativa de compatibilidad
Last-Modified e If-Unmodified-Since pueden expresar una condición relacionada: ejecuta el método solo si el recurso no ha cambiado desde la fecha proporcionada. Siguen siendo útiles cuando una API antigua ya publica horas de modificación y añadir etiquetas llevará tiempo.
Son más débiles para escrituras importantes. Las fechas HTTP tienen una precisión de un segundo. Dos cambios en el mismo segundo pueden producir la misma fecha visible y el cliente puede no saber si la marca almacenada por el servidor tiene una precisión mayor que la de su encabezado. La replicación, los relojes y la serialización añaden más formas de llevarse una sorpresa.
Si un cliente envía If-Match e If-Unmodified-Since, RFC 9110 da prioridad a If-Match. Es razonable: un validador fuerte ofrece una comprobación exacta de la versión; una fecha es una aproximación.
No construyas tu propio encabezado X-If-Version salvo que tengas una razón de protocolo que los encabezados estándar no puedan cubrir. Los encabezados personalizados se extienden rápidamente por SDK y proxies y después se convierten en trabajo permanente de compatibilidad. ETag e If-Match ya tienen una semántica clara, códigos de estado conocidos y compatibilidad con las herramientas HTTP habituales.
Los formatos PATCH también necesitan sus propias pruebas
Los encabezados condicionales protegen la versión del recurso. No validan si un parche expresa una transformación segura. Un JSON Merge Patch que incluya un objeto anidado completo aún puede borrar campos hermanos, incluso con un ETag correcto. Un JSON Patch puede apuntar a la posición equivocada de un array si la API modela una lista ordenada cuya composición cambió.
Usa el formato de parche que corresponda a la operación. JSON Patch, definido por RFC 6902, expresa operaciones como replace, add, remove y test sobre rutas concretas. Su operación test puede afirmar un valor dentro del documento antes de ejecutar las operaciones posteriores. JSON Merge Patch, definido por RFC 7396, describe un documento parcial deseado y trata null como una eliminación.
Un ETag a nivel de documento debe seguir siendo la protección externa. Añade un test de JSON Patch cuando la operación tenga un supuesto específico de campo que convenga hacer explícito:
[
{"op":"test","path":"/maintenanceMode","value":false},
{"op":"replace","path":"/replicas","value":5}
]
Si otro escritor cambió maintenanceMode antes de esta solicitud, la solicitud debe fallar en lugar de aumentar la capacidad durante el mantenimiento. La API debe documentar el error que devuelve cuando falla una prueba de JSON Patch. Muchas implementaciones usan 409 porque la instrucción del parche entra en conflicto con el documento actual, mientras que el desajuste del ETag externo sigue siendo un 412. La distinción resulta útil si los clientes necesitan saber si tenían un documento obsoleto o si hicieron una solicitud inválida dependiente del estado.
No dependas únicamente de una prueba test del parche como esquema general de concurrencia. Solo protege las rutas que recordaste probar. Un ETag fuerte protege la versión de la representación en la que el agente basó realmente su plan.
Los servidores deben aplicar la precondición en el límite de escritura
Un contrato de API que solo recomienda If-Match fallará cuando haya presión por cumplir un plazo. Un cliente lo omitirá, otro SDK olvidará reenviarlo y el endpoint vulnerable será el que los agentes descubran mediante ejemplos. Exígelo en las actualizaciones donde una sustitución obsoleta tenga un coste importante.
El controlador debe rechazar las condiciones ausentes antes de ejecutar efectos secundarios. Después debe pasar el validador esperado a la operación de almacenamiento que modifica el estado. Para un recurso respaldado por varias tablas o por un plano de control externo, envuelve la comparación y el cambio de estado en una sola transacción o usa la operación de comparación e intercambio del proveedor. Si el proveedor no puede hacerlo, tu API no puede prometer honestamente protección contra actualizaciones perdidas para esa escritura.
Prueba la carrera de forma deliberada. Inicializa un recurso en la revisión 7. Haz que los clientes A y B hagan GET. Permite que A ejecute PATCH con If-Match: "7" y verifica que recibe la revisión 8. Después deja que B ejecute PATCH con If-Match: "7" y verifica que recibe 412 y que su cambio previsto no aparece. Repite la prueba con DELETE, PUT completo y cualquier acción masiva que escriba un recurso a partir de una lectura anterior.
Prueba también los atajos peligrosos: un If-Match ausente debe recibir 428 en las rutas protegidas, If-Match: * no debe presentarse como protección contra escrituras obsoletas y un ETag débil no debe superar una comparación fuerte. Estas pruebas detectan regresiones que aparecen cuando un endpoint nuevo evita el método normal del repositorio.
Haz que el camino seguro sea más fácil que el de la sobrescritura
La API debe devolver ETag en cada GET de recursos mutables, documentar las condiciones necesarias junto a cada operación insegura y hacer que los métodos del SDK transporten los validadores de forma natural. Un cliente no debería tener que extraer encabezados sin procesar de un objeto de respuesta oculto para evitar dañar el trabajo de otro escritor.
En el caso de los agentes, separa la planificación de la ejecución. Lee el objetivo, registra su validador, describe la mutación prevista y emite la llamada condicional. Si cambia cualquier observación, descarta la escritura planificada salvo que el agente pueda demostrar que el cambio no es relevante. La regla suena conservadora porque lo es. La alternativa es dar a un proceso automatizado autoridad para actuar basándose en hechos que sabe que son antiguos.
Empieza por los endpoints donde una modificación sobrescrita despertaría a alguien: configuración de despliegues, control de acceso, registros de clientes, estado de pagos y metadatos de secretos. Añade If-Match, haz que una precondición ausente falle y prueba dos escritores contra la ruta. Cuando el servidor rechace por defecto las escrituras obsoletas, la velocidad de un agente dejará de convertir la concurrencia normal en daño silencioso.
FAQ
¿Qué es un ETag en una API?
Un ETag es un validador HTTP que identifica una representación concreta de un recurso. El cliente devuelve ese valor en If-Match cuando quiere que el servidor actualice o elimine únicamente la versión exacta que leyó antes. El servidor debe rechazar la escritura si el ETag actual es diferente.
¿Cuándo debe usar una API If-Match?
Usa If-Match en una actualización, sustitución, eliminación u otra operación que solo deba aplicarse a la versión que el cliente inspeccionó. Una etiqueta coincidente permite ejecutar el método; una etiqueta distinta debería producir 412 Precondition Failed. Es la protección estándar contra que un cliente obsoleto gane silenciosamente una carrera de escritura.
¿Cuál es la diferencia entre If-Match e If-None-Match?
If-Match sirve para la concurrencia optimista, mientras que If-None-Match suele impedir la creación o evitar la transferencia de una representación que no ha cambiado. En una solicitud insegura, If-None-Match: * significa «ejecuta esto solo si no existe ningún recurso actual». No uses If-None-Match como sustituto de una protección para operaciones de lectura, modificación y escritura.
¿Qué debe hacer un agente después de recibir una respuesta 412?
Una respuesta 412 Precondition Failed significa que la condición HTTP de la solicitud resultó falsa. El cliente debe obtener la representación actual, compararla con el cambio previsto y decidir si reintenta, combina los cambios o pide ayuda a una persona. Repetir la misma solicitud obsoleta solo repite el error.
¿Puedo usar un campo de versión en lugar de un ETag?
Un campo de versión puede funcionar si el servidor lo comprueba de forma atómica contra la revisión almacenada durante la escritura. A menudo resulta más fácil de inspeccionar y entender para los desarrolladores que un ETag opaco. No sustituye a los ETag cuando necesitas semántica condicional HTTP estándar para clientes genéricos y cachés.
¿Es seguro usar If-Unmodified-Since para controlar la concurrencia?
Por lo general, no. If-Unmodified-Since depende de marcas de tiempo, que pueden tener una precisión limitada o comportamientos problemáticos relacionados con los relojes. Puede servir como solución de compatibilidad, pero un ETag fuerte o un campo de versión comprobado de forma atómica ofrecen una prueba más segura para escrituras importantes.
¿Debe una API devolver 409 o 412 para una actualización obsoleta?
Un 409 Conflict informa de un conflicto de negocio o de estado que el cliente debe entender, como intentar cerrar una cuenta con una factura pendiente. Un 412 Precondition Failed indica que una precondición HTTP explícita era falsa. Devuelve ambos cuando describan errores distintos, en lugar de convertir cada escritura rechazada en un 409.
¿If-Match con asterisco evita las actualizaciones perdidas?
If-Match: * significa que cualquier representación actual es aceptable, por lo que solo protege contra la actualización de un recurso inexistente. No protege contra la sustitución de una versión más reciente. Envía el valor exacto del ETag cuando el cliente deba conservar los cambios de otro escritor.
¿Debe cada endpoint PATCH exigir un ETag?
Exige precondiciones en los endpoints donde una escritura obsoleta pueda cambiar dinero, permisos, estados de despliegue, datos de clientes o configuración. El servidor puede devolver 428 Precondition Required cuando el cliente omite la condición necesaria. No lo impongas sin criterio en comandos que solo añaden datos y no tienen una carrera de lectura, modificación y escritura.
¿Cómo debe almacenar las versiones de API un agente de IA de forma segura?
El agente debe mantener el ETag o la revisión vinculados a la representación que realmente leyó, enviarlos en la siguiente escritura protegida y descartarlos después de que falle una condición. Nunca debe inventar una etiqueta, reutilizar una de otro recurso ni resolver automáticamente un conflicto semántico sobrescribiendo el estado actual. Basta con un registro de trabajo de corta duración que contenga la URL del recurso, el validador observado, los campos previstos y la respuesta.