# Solicitações condicionais de API que impedem substituições feitas por agentes

Um agente pode gerar uma solicitação de API perfeitamente válida e ainda assim causar um dano real. A falha acontece quando ele lê um registro, outro ator altera esse registro e o agente depois grava sua cópia antiga por cima do estado mais novo. A autenticação não impede isso. A autorização também não. A solicitação veio de uma identidade autorizada, mas carregava uma visão obsoleta da realidade.

As solicitações condicionais de API corrigem esse problema específico. O cliente diz, na prática: «aplique esta alteração somente se o recurso continuar sendo a versão que observei». O servidor testa essa afirmação como parte da escrita. Se ela for falsa, o servidor recusa a ação antes de alterar qualquer coisa.

Esse contrato é ainda mais importante com agentes de programação do que com uma pessoa clicando em um formulário. Agentes podem ler muitos recursos, pausar para inspecionar código ou executar testes e depois fazer várias escritas quando o mundo já mudou. Trate toda atualização relevante como uma operação de leitura, alteração e escrita, a menos que a API possa provar que ela é um acréscimo ou um comando comutativo.

## Atualizações perdidas acontecem em fluxos comuns de leitura, alteração e escrita

Uma atualização perdida ocorre quando dois escritores começam do mesmo estado antigo e a escrita posterior apaga a anterior. Isso não exige uma indisponibilidade do banco de dados, um usuário mal-intencionado ou uma rede defeituosa. Basta um servidor que aceite uma substituição incondicional.

Considere uma configuração de implantação exposta como JSON:

```json
{
  "name": "billing-worker",
  "replicas": 3,
  "image": "registry.example/billing:2.4.0",
  "maintenanceMode": false
}
```

Um agente a lê para aumentar `replicas` de 3 para 5 antes de um teste de carga. Enquanto ele trabalha, um operador altera `maintenanceMode` para `true` para investigar um problema na fila. Se o agente enviar depois um `PUT` completo usando o documento salvo, poderá devolver `maintenanceMode` para `false`. A solicitação alterou as réplicas como esperado. Também desfez uma decisão de segurança que o agente nunca viu.

Uma atualização parcial reduz o alcance do problema, mas não elimina a disputa. Se um agente enviar um PATCH para substituir `/replicas`, esse campo ainda pode ter mudado desde a leitura. Mais importante, a decisão de definir as réplicas como 5 pode depender de campos que mudaram em outro lugar. PATCH descreve o formato do corpo da solicitação. Ele não informa de qual versão do recurso o corpo parte.

Por isso, «nossa interface só altera um campo» não é uma estratégia de concorrência. Uma interface pode esconder o problema por algum tempo porque as pessoas agem devagar e veem páginas atualizadas. Um processo autônomo não conta com essas proteções acidentais.

## Um ETag identifica a representação vista pelo cliente

O cabeçalho de resposta `ETag` é um validador HTTP. Quando o servidor retorna uma representação, pode anexar um token que identifica aquela versão da representação:

```http
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
}
```

A string não tem um formato interno obrigatório. Ela pode codificar uma revisão do banco de dados, um hash do conteúdo ou um valor opaco gerado pelo servidor. Os clientes devem tratá-la como opaca. Não analise uma tag para descobrir um número de revisão nem crie uma a partir de um corpo JSON. O servidor define seu significado.

A RFC 9110 define as tags de entidade e diferencia tags fortes de fracas. Um ETag forte tem a sintaxe comum entre aspas, como `"deploy-8f31c2"`. Ele indica que as representações correspondem byte a byte segundo a semântica de representação escolhida pelo servidor. Uma tag fraca começa com `W/`, como `W/"deploy-8f31c2"`, e indica apenas que duas representações são semanticamente semelhantes o suficiente para a validação de cache.

Essa diferença costuma ser ignorada. Validadores fracos servem para muitas verificações de cache em GET. Eles são inadequados para proteger uma escrita, porque duas representações «suficientemente próximas» ainda podem diferir em um campo que a escrita destruiria. A RFC 9110 exige que `If-Match` use comparação forte. Se sua API publica apenas ETags fracos, ela não oferece um ETag adequado à concorrência otimista.

Um recurso pode ter ETags diferentes para representações diferentes. JSON formatado, JSON compacto ou formatos negociados pelo conteúdo podem receber seus próprios validadores. Esse é um comportamento HTTP legítimo, mas inconveniente para clientes de API. Quando possível, mantenha uma representação canônica estável nos endpoints de escrita. Assim, o ETag recebido pelo cliente no GET continua fazendo sentido em PUT, PATCH e DELETE.

## If-Match transforma a verificação de versão em uma obrigação do servidor

`If-Match` coloca o ETag esperado em uma solicitação insegura. O servidor executa o método somente quando a representação atual corresponde fortemente à tag fornecida.

Um agente pode ler um registro e guardar o cabeçalho recebido:

```bash
curl -i \
  -H 'Authorization: Bearer $TOKEN' \
  https://api.example.test/v1/deployments/billing-worker
```

A resposta inclui:

```http
ETag: "deploy-8f31c2"
```

Depois, ele pode enviar somente a alteração pretendida, com o validador obtido nessa leitura:

```bash
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
```

Se o recurso continuar nessa versão, o servidor aplica o patch e retorna um ETag novo:

```http
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
}
```

Se a alteração do operador tiver mudado primeiro a versão atual, o servidor retornará:

```http
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."
}
```

A RFC 9110 determina que um servidor de origem não execute o método solicitado quando uma condição `If-Match` for avaliada como falsa. Essa é a propriedade que você está obtendo. A verificação precisa ocorrer na mesma operação atômica que faz a alteração. Um manipulador que lê a linha, compara uma revisão na memória da aplicação e só depois grava ainda mantém uma disputa entre a comparação e a escrita.

Em um banco de dados relacional, a implementação costuma ser uma atualização condicional:

```sql
UPDATE deployments
SET replicas = :replicas,
    revision = revision + 1
WHERE id = :id
  AND revision = :expected_revision;
```

Se a quantidade de linhas afetadas for zero, a API retorna 412. Se for um, a API retorna o documento atualizado e calcula seu próximo ETag a partir da nova revisão. Coloque o teste na cláusula `WHERE` ou em um primitivo transacional equivalente de comparação e atribuição. Não o divida em duas consultas separadas e chame isso de seguro.

## Campos de versão expõem o mesmo contrato nos dados da aplicação

Um campo de versão é um validador no nível da aplicação. Ele fornece aos clientes uma revisão visível, que eles enviam de volta no corpo da solicitação, na consulta ou em um cabeçalho específico. Isso pode ser mais fácil quando os clientes usam SDKs gerados, filas de mensagens ou protocolos que não preservam bem os cabeçalhos de resposta HTTP.

Um GET pode retornar:

```json
{
  "id": "billing-worker",
  "revision": 42,
  "replicas": 3,
  "maintenanceMode": false
}
```

A atualização pode declarar sua expectativa explicitamente:

```http
PATCH /v1/deployments/billing-worker HTTP/1.1
Content-Type: application/json

{
  "expectedRevision": 42,
  "replicas": 5
}
```

O servidor compara `expectedRevision` com a revisão armazenada de forma atômica. Em caso de sucesso, incrementa a revisão. Se houver divergência, rejeita a solicitação com uma resposta documentada, normalmente 412 quando o campo funciona como precondição.

Não confunda um campo de versão com uma marca de tempo. Uma revisão inteira monotônica torna a igualdade clara. Marcas de tempo criam perguntas complicadas: qual precisão o servidor armazena? Duas escritas podem cair no mesmo intervalo de precisão? A serialização alterou o valor? Uma réplica atribui o horário de maneira diferente? É possível resolver alguns desses problemas, mas um contador de revisão exige menos explicações.

ETags e campos de versão não competem entre si. Uma API pode expor os dois, com o ETag carregando a semântica HTTP padrão e a revisão ajudando o código da aplicação a exibir ou reconciliar alterações. Ambos precisam vir do mesmo estado confirmado. Se um indicar a versão 42 e o outro, por engano, se referir à versão 41, os clientes não terão uma forma confiável de se recuperar.

Evite aceitar um ETag ou um campo de revisão quando eles puderem discordar. Escolha uma precondição autoritativa para uma rota ou exija que ambos coincidam. Contratos de entrada flexíveis parecem amigáveis até que um cliente envie uma revisão obsoleta no corpo junto com um cabeçalho copiado recentemente e ninguém saiba qual afirmação o servidor aceitou.

## If-None-Match protege a criação, não a substituição obsoleta

`If-None-Match` inverte o predicado. Ele diz que o método só pode prosseguir se a representação atual não corresponder a nenhuma das tags fornecidas. Para métodos inseguros, uma condição falsa produz 412.

Sua forma mais útil para mutações é `If-None-Match: *`, que significa «crie isto somente se não existir uma representação atual». Um cliente pode tentar criar com segurança um recurso nomeado:

```bash
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
```

Se outro cliente já tiver criado esse trabalho, o servidor rejeita a solicitação em vez de substituí-lo silenciosamente. Isso é útil quando um agente derivou um identificador e não pode assumir um objeto existente com o mesmo nome.

Não envie `If-Match: *` para a concorrência otimista normal. Isso exige apenas que exista alguma representação atual. Dá ao agente permissão para substituir qualquer versão atual, inclusive uma que ele nunca leu. É proteção de existência, não proteção contra atualizações perdidas.

Para GET e HEAD, `If-None-Match` permite o uso de cache. Uma tag correspondente geralmente produz `304 Not Modified`, sem corpo na resposta. Esse comportamento de cache costuma ser o primeiro motivo pelo qual desenvolvedores encontram ETags. Não deixe que isso faça você tratar validadores como simples infraestrutura de cache. O mesmo mecanismo tem consequências muito mais importantes nas escritas.

## Uma resposta de escrita obsoleta precisa de uma política disciplinada para o agente

Um 412 deve interromper o plano de mutação atual. A premissa antiga do agente falhou, e reenviar a mesma solicitação não a torna verdadeira.

A sequência segura de recuperação é curta:

1. Busque a representação atual e seu novo validador.
2. Compare os campos ou as premissas de estado por trás da ação pretendida, não apenas o campo mencionado no patch.
3. Tente novamente com o novo validador somente se a intenção continuar correta sem precisar ser reinterpretada.
4. Peça aprovação ou pare quando o estado atual alterar o significado, o alcance ou o risco da ação.

É no segundo item que clientes automatizados costumam trapacear. Suponha que um agente planejasse remover um usuário de um grupo de acesso depois de ler uma lista de membros. Então uma pessoa muda a função do usuário de prestador de serviço para responsável por incidentes. Depois de buscar os dados novamente, o agente ainda consegue fazer uma remoção sintaticamente válida. Não deveria fazê-lo automaticamente, porque a mudança de função torna o plano original questionável.

Mantenha um registro de trabalho pequeno, mas completo: URI do recurso, ETag ou revisão observada, campos lidos, mutação pretendida e resposta. Um executor de ferramentas pode manter isso na memória durante uma tarefa curta. Um fluxo autônomo mais longo deve persistir essas informações em seu próprio estado de tarefa auditado. Nunca peça ao modelo para lembrar um validador apenas a partir de prosa. É fácil perder, alterar ou reutilizar valores de cabeçalho entre aspas no recurso errado.

O Sallyport pode manter o agente longe da credencial da API enquanto executa a chamada HTTP, mas o agente ainda precisa preservar e enviar o ETag como dados comuns da solicitação. Isolamento de credenciais e controle de concorrência resolvem falhas diferentes, portanto use ambos quando a ação tiver consequências.

## 412, 409 e 428 descrevem falhas diferentes

Retorne `412 Precondition Failed` quando o cliente tiver fornecido um cabeçalho de solicitação condicional ou uma precondição equivalente documentada, e essa condição for falsa. A resposta informa, com precisão, que o recurso já não está no estado afirmado pelo cliente.

Retorne `428 Precondition Required` quando o servidor exigir uma precondição para uma rota e o cliente a omitir. A RFC 6585 define esse status especificamente para evitar atualizações perdidas. A resposta pode informar que PATCH exige `If-Match` e incluir o ETag atual, desde que expô-lo não crie um problema de divulgação.

Retorne `409 Conflict` quando a solicitação entrar em conflito com o estado da aplicação mesmo depois de sua condição de versão passar. Por exemplo, um cliente pode enviar um valor `If-Match` correspondente à fatura atual, mas o servidor recusar o cancelamento porque a liquidação do pagamento já começou. O teste de versão passou, mas o comando de negócio ainda conflita com o estado da fatura.

Não reduza esses casos a um único erro genérico. O agente deve reagir de forma diferente:

- Depois de 428, busque o recurso e tente novamente com a condição exigida.
- Depois de 412, busque os dados outra vez e reavalie a intenção original.
- Depois de 409, analise o conflito do domínio e siga o caminho de resolução de negócio da API.

Um corpo de erro útil identifica o recurso, informa a condição que falhou sem repetir segredos e diz ao cliente se um novo GET pode ajudar. Ele não deve fingir que uma nova tentativa é inofensiva. O status HTTP fornece a categoria legível por máquina; o corpo oferece contexto suficiente para que o operador decida o próximo passo.

## Datas de última modificação são uma alternativa de compatibilidade

`Last-Modified` e `If-Unmodified-Since` podem expressar uma condição relacionada: execute o método somente se o recurso não tiver mudado desde a data fornecida. Eles continuam úteis quando uma API antiga já publica horários de modificação e adicionar tags levará tempo.

São mais fracos para escritas importantes. As datas HTTP têm precisão de um segundo. Duas alterações no mesmo segundo podem produzir a mesma data visível, e o cliente pode não saber se a marca de tempo armazenada pelo servidor tem precisão maior do que a exibida no cabeçalho. Replicação, relógios e serialização oferecem outras fontes de surpresa.

Se um cliente enviar `If-Match` e `If-Unmodified-Since`, a RFC 9110 dá precedência a `If-Match`. Isso faz sentido. Um validador forte oferece um teste exato de versão; uma data é uma aproximação.

Não crie seu próprio cabeçalho `X-If-Version` a menos que exista uma razão de protocolo que os cabeçalhos padrão não consigam atender. Cabeçalhos personalizados se espalham rapidamente por SDKs e proxies e depois se tornam trabalho permanente de compatibilidade. `ETag` e `If-Match` já têm semântica clara, códigos de status conhecidos e suporte nas ferramentas HTTP comuns.

## Os formatos PATCH também precisam de testes próprios

Os cabeçalhos condicionais protegem a versão do recurso. Eles não validam se um patch expressa uma transformação segura. Um JSON Merge Patch que inclua um objeto aninhado inteiro ainda pode apagar campos irmãos, mesmo com um ETag correto. Um JSON Patch pode apontar para a posição errada de um array se a API modelar uma lista ordenada cuja composição tenha mudado.

Use o formato de patch que corresponde à operação. O JSON Patch, definido pela RFC 6902, expressa operações como `replace`, `add`, `remove` e `test` em caminhos específicos. Sua operação `test` pode verificar um valor dentro do documento antes que as operações seguintes sejam executadas. O JSON Merge Patch, definido pela RFC 7396, descreve um documento parcial desejado e trata `null` como exclusão.

Um ETag no nível do documento deve continuar sendo a proteção externa. Adicione um `test` do JSON Patch quando a operação tiver uma premissa específica de campo que valha a pena deixar explícita:

```json
[
  {"op":"test","path":"/maintenanceMode","value":false},
  {"op":"replace","path":"/replicas","value":5}
]
```

Se outro escritor tiver alterado `maintenanceMode` antes dessa solicitação, ela deve falhar em vez de aumentar a capacidade durante a manutenção. A API deve documentar o erro retornado quando um teste do JSON Patch falhar. Muitas implementações usam 409 porque a instrução do patch conflita com o documento atual, enquanto a divergência do ETag externo continua sendo 412. Essa distinção é útil quando os clientes precisam saber se estavam com um documento obsoleto ou se fizeram uma solicitação inválida dependente do estado.

Não dependa apenas de um `test` do patch como estratégia geral de concorrência. Ele protege somente os caminhos que você se lembrou de testar. Um ETag forte protege a versão da representação na qual o agente realmente baseou seu plano.

## Os servidores precisam aplicar a precondição no limite da escrita

Um contrato de API que apenas recomenda `If-Match` falhará quando houver pressão por prazo. Um cliente o ignora, outro SDK esquece de encaminhá-lo e o endpoint vulnerável passa a ser justamente aquele que os agentes encontram nos exemplos. Exija-o em atualizações nas quais uma substituição obsoleta tenha custo significativo.

O manipulador deve rejeitar condições ausentes antes de executar efeitos colaterais. Depois, deve passar o validador esperado à operação de armazenamento que altera o estado. Para um recurso apoiado por várias tabelas ou por um plano de controle externo, envolva a comparação e a alteração do estado em uma única transação ou use a operação de comparação e atribuição do provedor. Se o provedor não puder fazer isso, sua API não pode prometer honestamente proteção contra atualizações perdidas nessa escrita.

Teste a disputa de propósito. Inicialize um recurso na revisão 7. Faça os clientes A e B obterem o recurso com GET. Deixe A executar PATCH com `If-Match: "7"` e confirme que recebe a revisão 8. Depois deixe B executar PATCH com `If-Match: "7"` e confirme que recebe 412 e que sua alteração pretendida não apareceu. Repita com DELETE, PUT completo e qualquer ação em lote que escreva um recurso com base em uma leitura anterior.

Teste também os atalhos perigosos: um `If-Match` ausente deve receber 428 em rotas protegidas, `If-Match: *` não deve ser apresentado como proteção contra escritas obsoletas e um ETag fraco não deve passar em uma comparação forte. Esses testes detectam regressões que surgem quando um novo endpoint ignora o método normal do repositório.

## Torne o caminho seguro mais fácil do que o caminho da substituição

A API deve retornar ETags em todo GET de recursos mutáveis, documentar as condições exigidas ao lado de cada operação insegura e fazer com que os métodos do SDK carreguem os validadores naturalmente. Um cliente não deveria precisar extrair cabeçalhos brutos de um objeto de resposta obscuro para evitar destruir o trabalho de outro escritor.

Para agentes, separe planejamento e execução. Leia o alvo, registre seu validador, descreva a mutação pretendida e faça a chamada condicional. Se qualquer observação mudar, descarte a escrita planejada, a menos que o agente consiga mostrar que a mudança é irrelevante. Essa regra parece conservadora porque é. A alternativa é dar a um processo automatizado autoridade para agir com base em fatos que ele sabe que estão obsoletos.

Comece pelos endpoints em que uma alteração substituída faria alguém ser acordado: configurações de implantação, controle de acesso, registros de clientes, estado de pagamentos e metadados de segredos. Adicione `If-Match`, faça uma precondição ausente falhar e teste dois escritores contra a rota. Quando o servidor passar a recusar escritas obsoletas por padrão, a velocidade de um agente deixará de transformar a concorrência comum em dano silencioso.
