7 min de leitura

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

Solicitações condicionais de API protegem atualizações feitas por agentes contra substituições obsoletas. Aprenda sobre ETags, campos de versão, If-Match, erros 412 e regras seguras para novas tentativas.

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:

{
  "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/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:

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

A resposta inclui:

ETag: "deploy-8f31c2"

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

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/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/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:

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:

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

A atualização pode declarar sua expectativa explicitamente:

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

Bloquear escritas no cofre
Enquanto o cofre estiver bloqueado, o Sallyport nega todas as ações do agente, incluindo mutações HTTP.

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:

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

Verificar o histórico de ações
Verifique offline a trilha de auditoria encadeada por hashes do Sallyport, sem precisar de uma chave do cofre.

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:

[
  {"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

Exigir aprovação para cada escrita
Exija Touch ID ou aprovação com um clique sempre que uma chave capaz de alterar o estado for usada.

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.

FAQ

O que é um ETag em uma API?

Um ETag é um validador HTTP que identifica uma representação específica de um recurso. O cliente envia esse valor de volta em If-Match quando quer que o servidor atualize ou exclua somente a versão exata que leu anteriormente. O servidor deve rejeitar a escrita se o ETag atual for diferente.

Quando uma API deve usar If-Match?

Use If-Match em uma atualização, substituição, exclusão ou outra operação que só deve ser aplicada à versão analisada pelo cliente. Uma tag correspondente permite a execução do método; uma tag diferente deve produzir 412 Precondition Failed. Essa é a proteção padrão contra um cliente com dados obsoletos vencer silenciosamente uma disputa de escrita.

Qual é a diferença entre If-Match e If-None-Match?

If-Match serve para concorrência otimista, enquanto If-None-Match normalmente impede a criação ou evita a transferência de uma representação que não mudou. Em uma solicitação insegura, If-None-Match: * significa «execute isto somente se não existir um recurso atual». Não use If-None-Match como substituto de uma proteção para operações de leitura, alteração e escrita.

O que um agente deve fazer depois de receber uma resposta 412?

Uma resposta 412 Precondition Failed significa que a condição HTTP da solicitação foi avaliada como falsa. O cliente deve buscar a representação atual, compará-la com a alteração pretendida e decidir se deve tentar novamente, mesclar as mudanças ou pedir a intervenção de uma pessoa. Repetir a mesma solicitação obsoleta apenas repete a falha.

Posso usar um campo de versão em vez de um ETag?

Um campo de versão pode funcionar se o servidor o comparar atomicamente com a revisão armazenada durante a escrita. Para desenvolvedores de aplicações, ele costuma ser mais fácil de inspecionar e entender do que um ETag opaco. Ele não substitui os ETags quando são necessárias semântica HTTP condicional padrão para clientes e caches genéricos.

If-Unmodified-Since é seguro para controlar concorrência?

Em geral, não. If-Unmodified-Since depende de marcas de tempo, que podem ter precisão limitada ou comportamentos complicados relacionados ao relógio. Pode ser um recurso útil de compatibilidade, mas um ETag forte ou um campo de versão atômico oferece um teste mais seguro para escritas importantes.

Uma atualização obsoleta deve retornar 409 ou 412?

Um 409 Conflict informa um conflito de negócio ou de estado que o cliente precisa entender, como tentar fechar uma conta com uma fatura em aberto. Um 412 Precondition Failed informa que uma precondição HTTP explícita era falsa. Retorne cada um quando representar uma falha diferente, em vez de transformar toda escrita rejeitada em 409.

If-Match com asterisco impede atualizações perdidas?

If-Match: * significa que qualquer representação atual é aceitável, portanto protege apenas contra a atualização de um recurso inexistente. Ele não protege contra a substituição de uma versão mais nova. Envie o valor exato do ETag quando o cliente precisar preservar as alterações de outro escritor.

Todo endpoint PATCH deve exigir um ETag?

Exija precondições em endpoints nos quais uma escrita obsoleta possa alterar dinheiro, permissões, estado de implantação, dados de clientes ou configurações. O servidor pode retornar 428 Precondition Required quando o cliente omitir a condição exigida. Não imponha isso automaticamente a comandos que apenas acrescentam dados e não têm uma disputa de leitura, alteração e escrita.

Como um agente de IA deve armazenar versões de API com segurança?

O agente deve manter o ETag ou a revisão vinculado à representação que realmente leu, enviá-lo na próxima escrita protegida e descartá-lo depois de qualquer condição falhar. Ele nunca deve inventar uma tag, reutilizar uma de outro recurso ou resolver automaticamente um conflito semântico substituindo o estado atual. Um registro de trabalho curto com a URL do recurso, o validador observado, os campos pretendidos e a resposta já é suficiente.

Sallyport

O Sallyport executa chamadas de API e comandos SSH pelo seu agente de IA. As chaves ficam em um cofre local no seu Mac; você aprova cada execução e toda ação vai para um registro selado.

© 2026 Sallyport · Código aberto sob Apache-2.0 · Oleg Sotnikov