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 |
| 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 | 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 |
|
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 |
|
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 |
|
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 |
|
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:
Capture uma requisição real e guarde o identificador da operação, seja
operationNameou hash, além das variáveis.Divida o build do frontend em blocos com um script, envie os blocos junto do identificador para
kimi-k3e 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".Observe um único ponto: ele vai diretamente à linha certa? Acerta, erra ou aponta para uma área próxima, porém incorreta?
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.
Teste sem cadastro: primeiro envie manualmente um trecho do build e veja se a faixa de contexto longo localiza o mapeamento em uma única passada; depois decida se vale integrá-la.
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.