AIREITER

API GraphQL sem query na requisição: duas formas de acessar uma operação persistida

Última Atualização: 2026-07-31 06:37:51

Você abre a aba Network do DevTools enquanto uma página baseada em GraphQL termina de carregar. Vasculha os corpos das requisições à procura da query { ... } desejada, mas ela simplesmente não está lá. Em vez disso, aparecem apenas um operationName, um hash de sessenta e quatro caracteres e um conjunto de variables.

Não foi distração sua. Trata-se de uma operação persistida: o cliente deixou de enviar a query em texto puro e passou a mandar somente um hash previamente registrado. No servidor, esse hash é consultado em um registro interno, a query real é recuperada e então executada. É nesse ponto que a abordagem de capturar pacotes deixa de resolver o problema. Você sabe qual operação foi chamada e quais variáveis ela recebeu, mas não enxerga os campos selecionados nem a estrutura esperada da resposta.

A reação inicial mais comum também costuma ser a errada: tentar "quebrar" o hash. Hash é unidirecional, não dá para quebrá-lo — e nem é preciso. O desafio real é classificar o cenário antes de agir. Há duas formas de obter o que você precisa, com uma diferença de custo de uma ordem de grandeza entre elas. Escolher a rota errada é desperdiçar esforço.

Por que a query desaparece em operações persistidas

Antes de decidir como lidar com isso, vale entender por que esse mecanismo existe.

Enviar GraphQL em texto puro tem um problema direto: a string da query pode ser longa, repetir toda a árvore de campos em cada requisição desperdiça banda, e o servidor precisa aceitar consultas arbitrárias, expondo toda a superfície do schema. Uma operação persistida resolve esses dois pontos. Durante o build, todas as queries usadas pelo cliente são extraídas, recebem hash e entram em uma whitelist no servidor. Em tempo de execução, o cliente envia apenas o hash e as variáveis; o servidor aceita somente hashes registrados e rejeita qualquer query fora da lista. É uma decisão legítima de desempenho, não uma medida criada especificamente contra scraping. A documentação de Automatic Persisted Queries da Apollo recomenda essa prática e usa o SHA-256 da query no lugar do texto puro. A ausência da query na captura é apenas uma consequência.

Para engenharia reversa, essa consequência é bem específica: a parte que define "o que buscar" saiu da requisição. Restam três elementos: um identificador da operação, que pode ser um hash ou um operationName legível; um conjunto de variáveis; e a resposta. A camada intermediária — os campos selecionados pela operação — não está na rede.

Em projetos reais, você encontra os dois extremos. De um lado, plataformas que nunca adotaram operações persistidas e continuam colocando a query em texto puro no corpo da requisição. Do outro, requisições reduzidas a envelopes opacos, nos quais não se lê sequer o nome de um campo. Cada caso pede uma abordagem diferente.

Duas estratégias, com custos muito diferentes

A primeira estratégia é localizar o texto puro ou a tabela de mapeamento dentro do build do cliente. A segunda é ignorar completamente o texto da query, tratar a operação como uma caixa-preta e reproduzi-la fielmente.

A primeira parece mais completa, então muita gente parte direto para ela. É justamente aí que começa boa parte do trabalho perdido. Ela só é barata quando o texto puro realmente foi entregue ao cliente — e essa premissa muitas vezes não se sustenta.

Caminho 1: localizar a query ou o mapeamento no build do cliente

O cenário de menor custo é aquele em que a query nunca foi escondida.

O hot-rank de uma plataforma chinesa de vídeos curtos funciona assim: existe um único endpoint /graphql, o corpo segue o trio padrão {operationName, variables, query}, o campo query contém a query GraphQL completa em texto puro e o operationName é um nome legível, como hotRankQuery. Não há nada a "obter": uma única captura já mostra tudo. A plataforma sequer adotou operações persistidas, representando o extremo mais simples desse espectro.

Um caso um pouco mais trabalhoso ocorre quando operações persistidas são usadas de fato, mas o cliente ainda carrega o mapeamento. Para enviar um hash, o cliente precisa saber qual operação corresponde a qual hash. Essa tabela de operationName para hash — às vezes acompanhada da query em texto puro — costuma estar embutida no bundle do frontend. Ferramentas de build frequentemente a geram como arquivo de manifesto ou a inserem diretamente em algum módulo. Ao encontrá-la, você obtém query e hash de uma vez, podendo depois adicionar campos ou alterar o conjunto de seleção.

A dificuldade não está em procurar em si, mas em lidar com um build de dezenas de milhares de linhas, minificado e ofuscado, com o mapeamento possivelmente fragmentado e embutido em vários trechos. É aqui que o modelo realmente ajuda, como veremos adiante. Trata-se de recuperação em blocos, não de raciocínio.

O teste para o caminho 1 é simples: se houver qualquer chance de a query ou o mapeamento estar no cliente, passe dez minutos procurando antes de avançar. Se encontrar, essa é a forma mais poderosa de obter a operação, porque você passa a ter controle total sobre o endpoint.

Caminho 2: reproduzir a operação como caixa-preta

O problema é que, muitas vezes, o texto puro não está no cliente.

Em uma implementação correta de operações persistidas, o cliente conserva somente o hash, enquanto a query em texto puro existe apenas no registro do servidor. Você pode revirar o bundle inteiro e não encontrará nada, porque esse conteúdo jamais foi distribuído. Insistir no caminho 1, nesse caso, é procurar algo que não existe.

O caminho 2 é uma solução subestimada: você não precisa da query em texto puro. O objetivo é obter a resposta, não conhecer a árvore de campos da query. Registre o identificador da operação — hash ou operationName — e o envelope de variáveis; então envie ambos sem alterações, trocando apenas os parâmetros de entrada que interessam. Você talvez nunca saiba quais campos foram selecionados, mas o servidor continuará retornando todos eles. Para a grande maioria das tarefas de coleta de dados e monitoramento, isso basta.

O innertube do YouTube é a forma clássica dessa abordagem. Nem sequer é GraphQL: há um conjunto fixo de endpoints autoexplicativos, youtubei/v1/{player,search,next}, e um corpo de requisição formado por um envelope context — tipo e versão do cliente — mais um conjunto de parâmetros. Ninguém tenta "reconstruir" o grafo interno de consultas do YouTube, algo que não é viável nem útil. O procedimento correto é ler uma vez a versão do cliente e o contexto nos recursos da página atual, carregar esse envelope sem modificações em cada requisição e substituir apenas entradas como videoId ou o termo de busca, sempre no endpoint fixo. A semântica da operação continua sendo uma caixa-preta. Esse caso, no qual um valor importante não está no código estático e precisa ser extraído de recursos em tempo de execução, é uma classe própria de problema de engenharia reversa, abordada separadamente.

A vantagem do caminho 2 é não depender de a query em texto puro estar no cliente. Seja um hash ou um envelope opaco, você não tenta entendê-lo: apenas o reproduz com fidelidade. O custo é ficar limitado às requisições que o cliente já faz. Se precisar de um campo que o cliente nunca solicita, a reprodução como caixa-preta não conseguirá entregá-lo.

Há ainda um caso que inviabiliza a reprodução: quando o envelope contém um campo de assinatura calculado novamente a cada requisição e que expira. A caixa-preta termina aí; será necessário entender esse campo isoladamente. Reconhecer a qual família de algoritmos essa assinatura pertence é o tema de outro artigo.

Em que ponto cada plataforma se encaixa

Colocando os dois casos reais lado a lado, fica claro onde cada um se encaixa e por quê:

Caso da plataforma

Formato da requisição

Há texto puro no cliente?

Abordagem natural

Motivo

GraphQL de hot-rank de vídeos curtos

/graphql + {operationName, variables, query}

Sim, o texto puro está no próprio corpo da requisição

Caminho 1 (custo quase zero)

Não usa operações persistidas; operationName legível, query em texto puro e uma captura contém tudo

YouTube innertube

Endpoints fixos + envelope context

Não há query em texto puro relevante

Caminho 2 (reprodução como caixa-preta)

Não é GraphQL, não há query a reconstruir; basta ler o envelope de contexto uma vez e reutilizá-lo sem alterações

O contraste entre esses extremos deixa uma coisa clara: a abordagem não é uma escolha livre, ela é determinada pelo design da API da plataforma. A primeira concentrou sua proteção em outros mecanismos — expor a query em texto puro não é problema nesse caso — e você a obtém quase incidentalmente. A segunda transformou o "o que buscar" em um envelope opaco; não há texto puro para perseguir, então só resta reproduzir a requisição.

O grande intervalo entre esses extremos, o GraphQL realmente persistido, é onde a avaliação importa. A query pode estar no cliente, em um mapeamento embutido no bundle, favorecendo o caminho 1; ou pode existir apenas no servidor, deixando o cliente apenas com o hash e levando ao caminho 2. É preciso classificar o caso antes de começar.

Antes de tudo, decida se você realmente precisa da query

A diferença de custo de uma ordem de grandeza depende inteiramente dessa decisão.

Se uma plataforma envia somente o hash e mantém a query no servidor, insistir no caminho 1 para recuperar o texto puro pode significar passar dias escavando o bundle e descobrir, no fim, que aquilo nunca foi distribuído. Não é uma questão de dificuldade; é uma questão de direção. Nenhuma quantidade de esforço produzirá resultado.

O inverso também vale: se você precisa alterar a query, solicitando um campo que o cliente jamais pede, a reprodução como caixa-preta do caminho 2 não resolve. Você volta a depender do caminho 1 para conseguir o texto puro — e fica bloqueado se não houver como obtê-lo.

Por isso, a pergunta inicial não deve ser "como consigo a query?", mas sim: eu realmente preciso da query em texto puro?

  • Só precisa repetir uma requisição que o cliente já faz e ler a resposta: use o caminho 2, a reprodução como caixa-preta. É a alternativa mais barata, mais ignorada e funciona com ou sem query disponível. Este deve ser o padrão.

  • Precisa alterar o conjunto de seleção ou montar uma query que o cliente nunca envia: o caminho 1, recuperar o texto puro, passa a ser obrigatório. O custo dependerá de o cliente ter distribuído ou não o mapeamento. Se não distribuiu, ocorre o salto de custo de uma ordem de grandeza: você aceita isso ou reconsidera se realmente precisa modificar a query.

Fazer esse julgamento logo no início evita grande parte do desperdício típico de seguir pelo caminho 1, ficar três dias travado e perceber tarde demais que o caminho 2 era o adequado. Esse trade-off entre reescrever o protocolo ou tolerar a caixa-preta é o tema do artigo sobre a escada de purificação; aqui, ele apenas determina qual rota usar.

Encontrar o mapeamento é recuperação em blocos, não raciocínio

O passo tecnicamente mais exigente do caminho 1 é localizar a declaração da operação e o mapeamento em dezenas de milhares de linhas do build do frontend. É exatamente aí que um modelo pode economizar tempo de verdade. Mas primeiro é importante entender que tipo de tarefa é essa.

Não é uma tarefa de raciocínio. Você não precisa que o modelo compreenda o que o código calcula; precisa que ele localize, em um volume enorme de texto, qual bloco declara o mapeamento de operationName para hash, qual módulo incorpora a query em texto puro ou onde o envelope de contexto é montado. Isso é recuperação em blocos. O fator decisivo é conseguir colocar contexto suficiente de uma vez e apontar com precisão dentro dele, não a capacidade de elaborar argumentos complexos.

Faça primeiro a etapa mecânica de divisão: use um script para separar o build em módulos, indexá-lo e filtrar polyfills e módulos de negócio sem relação com o problema. Depois entregue o resultado ao modelo. A tarefa passa a ser simplesmente: "encontre a declaração nestes blocos".

Nesse fluxo, as quatro faixas se separam claramente por capacidade:

Etapa

Capacidade necessária

Escolha

model id

Localizar o mapeamento ou a declaração da operação em dezenas de milhares de linhas

Contexto longo, capaz de absorver um grande trecho do build e apontar com precisão

Kimi K3

kimi-k3

Decidir entre caminho 1 e caminho 2; inferir, a partir de algumas amostras, quais campos do envelope variam

Raciocínio forte, leitura de estrutura e avaliação de trade-offs

Claude Opus 5

claude-opus-5

Rotular centenas de operações em lote, gerar stubs de reprodução e preencher tipos de variáveis

Baixo custo e alta concorrência

Claude Sonnet 5

claude-sonnet-5

Quando a reprodução não conecta, ler o diff para atribuir a causa: campo de contexto ausente? versão do hash alterada?

Raciocínio intermediário, com explicação baseada na diferença de resposta

GPT-5.6 Sol

gpt-5.6-sol

A primeira faixa é o foco deste artigo. Trocar de modelo na etapa de localização muda visivelmente o resultado, porque o limite aqui é a janela de contexto. O build tem dezenas de milhares de linhas; um modelo de contexto curto não consegue acomodá-lo e precisa truncar o conteúdo. Um único corte pode excluir o mapeamento. Quando o modelo diz que não encontrou, não é necessariamente porque não sabe buscar, mas porque nunca teve acesso ao trecho certo.

Em vez de aceitar essa diferença por confiança, teste:

  1. Capture uma requisição real e guarde o identificador da operação, seja operationName ou hash, além das variáveis.

  2. Divida o build do frontend em blocos com um script, envie os blocos junto do identificador para kimi-k3 e peça que ele localize "onde essa operação é declarada e qual bloco contém a query correspondente em texto puro ou o mapeamento de hash".

  3. Observe um único ponto: ele vai diretamente à linha certa? Acerta, erra ou aponta para uma área próxima, porém incorreta?

  4. Como controle, envie a mesma entrada a um modelo de contexto curto e verifique se ele falha porque não consegue acomodar o material. A taxa de acerto é seu critério de escolha.

Uma rodada já mostra que, nesse tipo de tarefa, contexto longo não é apenas "um pouco melhor": é a diferença entre conseguir e não conseguir.

O maior problema é o custo de trocar de modelo

São quatro modelos de três fornecedores, três SDKs, três esquemas de autenticação e três formatos de erro. Para usar modelos diferentes em recuperação, julgamento, processamento em lote e atribuição de falhas, a abordagem ingênua é integrar os três clientes. Muita gente faz as contas, conclui que não vale a pena e acaba usando um único modelo em todo o processo — inclusive uma faixa incapaz de acomodar o build na etapa de recuperação de contexto longo — para depois culpar o modelo por não encontrar o mapeamento.

AIReiter elimina essa camada: uma chave, uma interface compatível com OpenAI e as quatro faixas disponíveis por trás dela. Para trocar de modelo, basta alterar o campo model no corpo da requisição.

# Locate the mapping: the long-context tier, swallows a big chunked build at once
curl https://aireiter.com/api/v1/chat/completions \
  -H "Authorization: Bearer $AIREITER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "kimi-k3",
    "messages": [{"role": "user", "content": "<chunked frontend build + the operation identifier to locate>"}]
  }'

# Label operations / generate replay stubs in bulk: change the model field, leave the rest
#   "model": "claude-sonnet-5"
# Replay attribution:
#   "model": "gpt-5.6-sol"

Se você já usa o SDK da OpenAI, aponte base_url para https://aireiter.com/api/v1 e não mude mais nada. No SDK da Anthropic, use POST /api/v1/messages com a mesma chave.

Em preço, os modelos Claude têm 30% de desconto sobre o valor de tabela, os modelos GPT custam metade e o Kimi K3 também pode ser chamado com a mesma chave. O custo deste fluxo se concentra em dois pontos: a localização do caminho 1, que envia todo o build fragmentado em entradas de algumas centenas de milhares de tokens para kimi-k3; e a rotulagem de centenas de operações com geração de stubs de reprodução em lote, a etapa com maior volume de chamadas, feita no claude-sonnet-5 com 30% de desconto. O desconto incide exatamente sobre o lote mais intenso em chamadas.

Conclusão

Uma API GraphQL sem documentação não significa que você não consegue integrá-la. Uma operação persistida apenas retirou da requisição a definição de "o que buscar" e a colocou em um de dois lugares: no build do cliente, onde você pode encontrá-la pelo caminho 1; ou somente no servidor, caso em que não vale perseguir o texto puro e a operação deve ser reproduzida como caixa-preta pelo caminho 2.

Esses dois caminhos têm custos separados por uma ordem de grandeza. O que define a escolha não é qual deles parece mais completo, mas duas perguntas anteriores: a query em texto puro está no cliente? E você precisa alterar a query? Responder a ambas antes de começar elimina grande parte do esforço desperdiçado.

O papel do modelo aqui é específico: no caminho 1, localizar uma declaração em dezenas de milhares de linhas é uma tarefa pura de recuperação. Uma faixa de contexto longo absorve esse material de uma vez e aponta o local correto, transformando dias de escavação em minutos. Ela não decide por você qual caminho seguir — esse é o julgamento que este artigo busca orientar —, apenas executa o trabalho pesado de encontrar o mapeamento.