Vous ouvrez l’onglet Network des DevTools, observez le chargement d’une page propulsée par GraphQL, puis inspectez chaque corps de requête à la recherche du query { ... } attendu. Rien. Seulement un operationName, un hash de soixante-quatre caractères et un ensemble de variables.
Vous n’avez rien raté : il s’agit d’une opération persistée. Le client n’envoie plus la requête GraphQL en clair, mais uniquement un hash déjà enregistré. Côté serveur, ce hash est retrouvé dans un registre, la requête réelle est reconstruite puis exécutée. La capture réseau ne permet donc plus d’aller plus loin : elle révèle l’opération appelée et ses variables, mais ni les champs sélectionnés ni la structure de la réponse.
Le premier réflexe est souvent le mauvais : vouloir « casser » le hash. Un hash est à sens unique, il ne se casse pas, et ce n’est de toute façon pas le sujet. Le vrai travail consiste à qualifier la situation avant d’agir. Deux chemins existent pour récupérer ce qu’il vous faut, avec un coût qui peut varier d’un ordre de grandeur. Choisir le mauvais, c’est gaspiller son temps.
Pourquoi une opération persistée masque la requête
Il faut d’abord comprendre la raison d’être du mécanisme, faute de quoi il est difficile de choisir la bonne approche.
Le GraphQL en clair a un défaut évident : les requêtes sont longues, répéter tout l’arbre de champs à chaque appel est coûteux, et le serveur doit accepter des requêtes arbitraires, ce qui expose toute la surface du schéma. Les opérations persistées répondent à ces deux problèmes. À la compilation, toutes les requêtes utilisées par le client sont extraites, hachées et enregistrées côté serveur comme liste blanche. À l’exécution, le client envoie le hash et les variables ; le serveur n’accepte que les hashes enregistrés et rejette toute requête absente de cette liste. C’est un vrai choix de performance, pas une mesure anti-scraping délibérée. La documentation des Automatic Persisted Queries d’Apollo le recommande d’ailleurs, avec un SHA-256 de la requête à la place du texte en clair. L’absence de requête dans la capture n’est qu’une conséquence de ce modèle.
En rétro-ingénierie, cette conséquence est très concrète : le « quoi récupérer » a disparu du trafic réseau. Il reste trois éléments : un identifiant d’opération — hash ou operationName lisible —, des variables et une réponse. La couche intermédiaire, celle qui décrit les champs sélectionnés, n’est plus visible sur le réseau.
Dans la pratique, on rencontre les deux extrêmes. D’un côté, des plateformes qui n’ont jamais adopté les opérations persistées : la requête en clair est toujours dans le corps de la requête. De l’autre, des appels réduits à une enveloppe opaque, sans même un nom de champ lisible. Ces deux cas demandent des méthodes différentes.
Deux méthodes, des coûts très éloignés
Première méthode : retrouver la requête en clair ou sa table de correspondance dans le build client. Deuxième méthode : ne pas chercher le texte de la requête et rejouer l’opération telle quelle, en boîte noire.
La première paraît plus complète, et c’est pourquoi beaucoup s’y engagent par défaut. C’est aussi là que commence le temps perdu : elle n’est économique que si le texte a réellement été livré au client, ce qui est loin d’être systématique.
Méthode 1 : retrouver la requête ou son mapping dans le build client
Le cas le moins coûteux est celui où la requête n’a jamais été masquée.
Le classement des tendances d’une plateforme chinoise de vidéos courtes fonctionne ainsi : un unique endpoint /graphql, un corps standard {operationName, variables, query}, un champ query qui contient l’intégralité du GraphQL en clair, et un operationName lisible tel que hotRankQuery. Il n’y a rien à récupérer : une seule capture fournit tout. La plateforme n’a tout simplement pas adopté les opérations persistées ; c’est l’extrémité la plus simple du spectre.
Un cas légèrement plus complexe utilise bien des opérations persistées, mais conserve encore le mapping côté client. Pour envoyer un hash, le client doit savoir à quelle opération il correspond. Cette table de correspondance entre operationName et hash — parfois accompagnée de la requête en clair — est généralement intégrée au bundle frontend. Les outils de build peuvent la produire sous forme de manifeste ou l’inliner dans un module. Si vous la retrouvez, vous obtenez à la fois la requête et son hash, avec la liberté d’ajouter des champs ou de modifier le jeu de sélection.
La difficulté n’est pas tant de chercher que de chercher dans un build minifié et obfusqué de plusieurs dizaines de milliers de lignes, où le mapping peut être fragmenté et inliné. C’est précisément l’étape où le modèle est utile, comme nous le verrons plus bas. Il s’agit de recherche par fragments, pas de raisonnement.
Le test pour la méthode 1 est simple : si le texte ou le mapping est présent quelque part dans le client, consacrez d’abord dix minutes à le retrouver. Si vous le trouvez, c’est l’approche la plus solide et vous gardez un contrôle total sur l’endpoint.
Méthode 2 : rejouer l’opération sans chercher le texte
Le problème, c’est que la requête en clair n’est souvent jamais présente dans le client.
Une opération persistée correctement mise en œuvre ne laisse au client que le hash ; la requête en clair vit exclusivement dans le registre du serveur. Vous pouvez fouiller le bundle de fond en comble sans rien trouver, parce que cette requête n’a jamais été distribuée. S’acharner avec la méthode 1 revient alors à chercher un objet qui n’existe pas.
La méthode 2 est sous-estimée : vous n’avez pas nécessairement besoin de connaître le texte de la requête. Ce qui vous intéresse est la réponse, pas l’arbre de champs sélectionnés. Enregistrez donc l’identifiant de l’opération — hash ou operationName — et son enveloppe de variables, rejouez-les à l’identique, puis ne modifiez que les paramètres d’entrée qui vous concernent. Vous ne saurez pas quels champs ont été demandés, mais le serveur vous les renverra quand même. Pour la grande majorité des tâches de collecte et de monitoring, cela suffit.
L’innertube de YouTube représente la forme classique de cette approche. Ce n’est même pas du GraphQL : il s’agit d’un ensemble fixe d’endpoints explicites, youtubei/v1/{player,search,next}, avec un corps contenant une enveloppe context — type et version du client — ainsi que des paramètres. Personne ne tente de « reconstruire » le graphe de requêtes interne de YouTube : ce serait à la fois impossible et inutile. La bonne approche consiste à lire une fois la version et le contexte client dans les ressources de la page courante, puis à réutiliser cette enveloppe à l’identique pour chaque requête, en remplaçant seulement des entrées telles que videoId ou le terme recherché, vers l’endpoint fixe concerné. La sémantique de l’opération reste une boîte noire. Ce cas, dans lequel une valeur clé n’est pas présente dans le code statique et doit être extraite des ressources à l’exécution, forme une catégorie de rétro-ingénierie distincte, traitée séparément.
L’avantage de la méthode 2 est son indépendance vis-à-vis du texte de la requête. Hash ou enveloppe opaque, vous ne cherchez pas à les comprendre : vous les reproduisez fidèlement. En contrepartie, vous restez limité aux requêtes déjà effectuées par le client. Si vous voulez un champ que le client ne demande jamais, le rejeu en boîte noire ne pourra pas vous le fournir.
Un autre cas fait échouer le rejeu : l’enveloppe inclut un champ de signature recalculé pour chaque requête et qui expire. La boîte noire s’arrête là ; il faut alors isoler ce champ et comprendre son fonctionnement. Identifier la famille de l’algorithme de signature relève d’un autre article.
Où se situent les plateformes, et pourquoi
Ces deux cas réels placés dans un même tableau rendent le choix et sa logique immédiats :
Cas de plateforme | Forme de la requête | Texte en clair dans le client ? | Approche naturelle | Pourquoi |
|---|---|---|---|---|
GraphQL de classement tendance d’une plateforme de vidéos courtes |
| Oui, directement dans le corps de la requête | Méthode 1 (coût quasi nul) | Pas d’opérations persistées, operationName lisible, requête en clair : une capture suffit |
YouTube innertube | Endpoints fixes + enveloppe | Aucune requête en clair à proprement parler | Méthode 2 (rejeu en boîte noire) | Ce n’est pas du GraphQL, il n’y a pas de requête à reconstruire ; lire une fois l’enveloppe de contexte puis la réutiliser telle quelle |
Le contraste entre ces deux extrêmes montre une chose : la méthode ne dépend pas de votre préférence, mais de l’architecture API de la plateforme. La première a choisi de protéger autrement son service et peut laisser la requête visible. La seconde transforme le « quoi récupérer » en enveloppe opaque : il n’y a aucun texte à poursuivre, seulement une requête à rejouer.
Entre les deux se trouve le vaste terrain du vrai GraphQL persisté, là où le diagnostic compte le plus. La requête en clair peut être côté client — mapping intégré au bundle, méthode 1 — ou uniquement côté serveur — le client ne possède que le hash, méthode 2. Il faut déterminer lequel des deux cas s’applique avant de commencer.
Le bon réflexe : déterminer si le texte en clair est nécessaire
L’écart de coût d’un ordre de grandeur repose entièrement sur ce jugement initial.
Si une plateforme n’envoie réellement que le hash et garde la requête en clair sur le serveur, s’obstiner à appliquer la méthode 1 peut vous faire passer des jours dans le bundle pour découvrir que ce que vous cherchiez n’a jamais été distribué. Ce n’est pas un problème de difficulté, mais de direction : aucun effort supplémentaire ne produira de résultat.
À l’inverse, si vous devez modifier la requête — pour demander un champ que le client ne sélectionne jamais —, le rejeu en boîte noire de la méthode 2 ne suffit pas. Il faut revenir à la méthode 1 pour récupérer le texte, et vous êtes bloqué si ce texte reste inaccessible.
L’ordre de réflexion ne devrait donc pas être « comment récupérer la requête ? », mais commencer par une question : ai-je réellement besoin de la requête en clair ?
Vous voulez simplement reproduire une requête déjà émise par le client et lire sa réponse : méthode 2, le rejeu en boîte noire. C’est l’option la moins coûteuse, la plus négligée, et elle fonctionne que le texte soit disponible ou non. C’est le choix par défaut.
Vous devez modifier le jeu de sélection ou construire une requête que le client n’envoie jamais : la méthode 1, qui récupère le texte en clair, est indispensable. Son coût dépend de la présence ou non du mapping dans le client. S’il n’y est pas, le coût explose d’un ordre de grandeur : vous devez l’accepter ou réévaluer votre besoin de modifier la requête.
Faire ce diagnostic en premier évite la plupart des scénarios où l’on s’engage dans la méthode 1 pour rester bloqué trois jours avant de comprendre qu’il fallait rejouer en boîte noire. Ce compromis entre réécriture et acceptation de la boîte noire est abordé dans l’article sur l’échelle de purification ; ici, il sert simplement à choisir la bonne méthode.
Trouver le mapping : une recherche par fragments, pas un problème de raisonnement
L’étape techniquement exigeante de la méthode 1 consiste à localiser la déclaration de l’opération et son mapping dans plusieurs dizaines de milliers de lignes de build frontend. C’est exactement là qu’un modèle peut faire gagner un temps réel. Mais il faut d’abord bien identifier la nature de la tâche.
Ce n’est pas une tâche de raisonnement. Le modèle n’a pas besoin de comprendre le calcul du code ; il doit localiser, dans un vaste corpus, le bloc qui déclare le mapping entre operationName et hash, le module qui inline une requête en clair ou l’endroit où l’enveloppe de contexte est construite. C’est de la recherche par fragments. La performance dépend de la quantité de contexte qu’il peut absorber et de sa capacité à pointer précisément un endroit, non de sa faculté à débattre avec lui-même.
Commencez par l’étape mécanique de découpage : utilisez un script pour séparer le build en modules, l’indexer et écarter les polyfills ainsi que les modules métier sans rapport. Donnez ce résultat au modèle ; la tâche devient alors simplement : « trouver la déclaration dans ces blocs ».
Les quatre niveaux se distinguent clairement par les capacités requises dans ce flux :
Étape | Capacité requise | Choix | model id |
|---|---|---|---|
Localiser le mapping / la déclaration d’opération dans plusieurs dizaines de milliers de lignes | Contexte long, capable d’absorber une grande portion du build et de pointer précisément | Kimi K3 |
|
Choisir la méthode 1 ou 2, déduire de quelques échantillons quels champs de l’enveloppe varient | Raisonnement solide, lecture de structure et arbitrage | Claude Opus 5 |
|
Étiqueter des centaines d’opérations en lot, générer des stubs de rejeu, compléter les types de variables | Faible coût, forte concurrence | Claude Sonnet 5 |
|
Quand le rejeu échoue, analyser le diff pour en attribuer la cause (champ de contexte manquant ? version du hash modifiée ?) | Raisonnement intermédiaire, explication à partir de la différence de réponse | GPT-5.6 Sol |
|
Le premier niveau est le cœur de cet article. Changer de modèle à l’étape de localisation modifie visiblement le résultat, parce que la limite est celle de la fenêtre de contexte. Le build compte plusieurs dizaines de milliers de lignes : un modèle à contexte court ne peut pas l’intégrer entièrement et doit tronquer. Une seule troncature peut exclure le mapping ; son « je ne le trouve pas » ne signifie alors pas qu’il ne sait pas chercher, mais qu’il ne l’a jamais vu.
Ne me croyez pas sur parole : testez-le.
Capturez une requête réelle et conservez son identifiant d’opération — operationName ou hash — ainsi que ses variables.
Découpez le build frontend avec un script, envoyez-le avec cet identifiant à
kimi-k3, puis demandez-lui de localiser « l’endroit où cette opération est déclarée, ainsi que le bloc contenant la requête en clair ou le mapping de hash correspondant ».Vérifiez une seule chose : pointe-t-il directement la bonne ligne ? Réussite, échec, ou emplacement voisin mais erroné.
Pour le contrôle, fournissez exactement la même entrée à un modèle à contexte court et voyez s’il échoue faute de pouvoir tout intégrer. Le taux de réussite devient votre critère de sélection.
Un seul essai montre que, pour cette classe de tâche, le contexte long n’est pas « un peu meilleur » : c’est la différence entre pouvoir le faire et ne pas pouvoir le faire.
Le vrai frein, c’est le coût du changement de modèle
Quatre modèles issus de trois fournisseurs, trois SDK, trois schémas d’authentification et trois formats d’erreur. Pour employer un modèle différent pour la recherche, le jugement, les traitements par lots et l’attribution, la solution naïve consiste à intégrer les trois clients. Beaucoup font le calcul, estiment que cela n’en vaut pas la peine et utilisent finalement un seul modèle partout — y compris un niveau incapable d’absorber le build à l’étape de recherche long contexte — avant de conclure que « le modèle ne trouve pas ».
AIReiter aplatit cette couche : une clé, une interface compatible OpenAI, les quatre niveaux derrière, et un changement de modèle qui se résume au champ model du corps de requête.
# Localiser le mapping : niveau long contexte, capable d’absorber une grande portion du build découpé
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": "<build frontend découpé + identifiant de l’opération à localiser>"}]
}'
# Étiqueter les opérations / générer des stubs de rejeu en lot : changez le champ model, rien d’autre
# "model": "claude-sonnet-5"
# Attribution lors du rejeu :
# "model": "gpt-5.6-sol"
Si vous utilisez déjà le SDK OpenAI, pointez base_url vers https://aireiter.com/api/v1 sans rien changer d’autre. Avec le SDK Anthropic, utilisez POST /api/v1/messages avec la même clé.
Côté tarifs, les modèles Claude sont facturés avec 30 % de remise sur le prix catalogue, les modèles GPT à moitié prix, et Kimi K3 est accessible avec la même clé. Le coût de ce flux se concentre à deux endroits : la localisation de la méthode 1, qui envoie l’ensemble du build découpé à raison de quelques centaines de milliers de tokens par entrée sur kimi-k3 ; puis l’étiquetage de centaines d’opérations et la génération en lot de stubs de rejeu, l’étape la plus intensive en appels, sur claude-sonnet-5 avec 30 % de remise. La réduction s’applique donc précisément à l’étape la plus dense en appels.
Essayer sans inscription : commencez par envoyer manuellement un fragment de build et vérifiez si le niveau long contexte localise le mapping en un seul passage, puis décidez si vous souhaitez l’intégrer.
En conclusion
Une API GraphQL sans documentation ne signifie pas qu’elle est impossible à intégrer. Une opération persistée a seulement retiré le « quoi récupérer » de la requête pour le placer à l’un de deux endroits : dans le build client — il faut le retrouver, méthode 1 — ou uniquement sur le serveur — il ne faut pas poursuivre le texte en clair, mais rejouer l’opération en boîte noire, méthode 2.
Le coût de ces deux méthodes diffère d’un ordre de grandeur. Le choix ne dépend jamais de celle qui paraît la plus complète, mais de deux questions préalables : le texte en clair est-il présent dans le client, et devez-vous modifier la requête ? Répondez-y avant de commencer pour éviter l’essentiel du travail perdu.
Le rôle du modèle est ici précis : dans la méthode 1, localiser une déclaration parmi plusieurs dizaines de milliers de lignes relève de la recherche pure. Un niveau long contexte peut absorber le tout en un passage et pointer l’emplacement exact, transformant des jours de fouille en quelques minutes. Il ne décide pas à votre place quelle méthode employer — c’est le jugement que cet article doit vous permettre de porter — ; il prend simplement en charge le travail répétitif de localisation du mapping.