AIREITER

GraphQL-API ohne Query im Request: Zwei Wege zur Persisted Operation

Zuletzt aktualisiert: 2026-07-31 06:35:50

DevTools geöffnet, Network-Tab im Blick, die GraphQL-Seite lädt fertig – und im Request-Body fehlt genau das, was man sucht: query { ... }. Stattdessen gibt es nur einen operationName, einen Hash mit 64 Zeichen und ein Paket voller variables.

Das ist kein übersehener Eintrag, sondern eine Persisted Operation. Der Client sendet die Query nicht mehr im Klartext, sondern nur noch einen zuvor registrierten Hash. Der Server schlägt diesen Hash in seiner Registry nach, rekonstruiert daraus die eigentliche Query und führt sie aus. Damit endet der klassische Weg über den Packet Capture: Sichtbar bleiben die aufgerufene Operation und ihre Variablen, nicht aber die abgefragten Felder oder die Struktur der Antwort.

Der naheliegende erste Impuls ist meist der falsche: den Hash „knacken“ zu wollen. Hashes sind Einwegfunktionen, sie lassen sich nicht knacken – und nötig ist das auch nicht. Entscheidend ist zunächst die Einordnung: In welcher Situation steckt man überhaupt? Davon hängt ab, welcher Weg sinnvoll ist. Zwischen den beiden Ansätzen liegt beim Aufwand eine ganze Größenordnung.

Warum Persisted Operations im Mitschnitt keine Query zeigen

Bevor es um die Vorgehensweise geht, lohnt sich ein Blick auf den Zweck dieses Mechanismus.

GraphQL im Klartext hat offensichtliche Nachteile: Die Query-Strings sind lang, bei jedem Request wird der komplette Feldbaum übertragen, und der Server muss grundsätzlich beliebige Queries akzeptieren. Das vergrößert die Angriffsfläche des gesamten Schemas. Persisted Operations lösen beides. Beim Build werden alle vom Client benötigten Queries extrahiert, gehasht und serverseitig als Whitelist registriert. Zur Laufzeit schickt der Client nur noch Hash und Variablen. Der Server akzeptiert ausschließlich bekannte Hashes; alles andere wird abgewiesen. Das ist in erster Linie ein Performance- und Sicherheitsdesign, keine gezielte Anti-Scraping-Maßnahme. Die Dokumentation zu Apollo Automatic Persisted Queries empfiehlt genau diesen Ansatz und verwendet den SHA-256-Hash der Query anstelle des Klartexts. Dass die Query im Mitschnitt fehlt, ist lediglich die Folge davon.

Für Reverse Engineering ist diese Folge allerdings entscheidend: Das „was soll abgefragt werden?“ wurde aus dem Request herausgezogen. Übrig bleiben drei Dinge: ein Operations-Identifier – Hash oder lesbarer operationName –, ein Satz Variablen und die Antwort. Die Zwischenschicht, also welche Felder die Operation auswählt, ist auf dem Wire nicht mehr vorhanden.

In der Praxis begegnet man beiden Enden des Spektrums. Manche Plattformen nutzen gar keine Persisted Operations; die Klartext-Query steht weiterhin offen im Request-Body. Andere verdichten den Request zu einem undurchsichtigen Envelope, in dem nicht einmal Feldnamen lesbar sind. Für beide Fälle braucht es unterschiedliche Vorgehensweisen.

Zwei Vorgehensweisen mit deutlich unterschiedlichem Aufwand

Beim ersten Weg liegt die Klartext-Query oder eine Zuordnungstabelle im Client-Build und wird dort gefunden. Beim zweiten sucht man gar nicht erst nach Klartext, sondern behandelt die komplette Operation als Black Box und spielt sie unverändert erneut ab.

Der erste Weg klingt vollständiger, deshalb stürzen sich viele reflexhaft darauf. Genau dort beginnt oft die Zeitverschwendung. Günstig ist er nur, wenn der Klartext tatsächlich an den Client ausgeliefert wurde – und das ist längst nicht immer der Fall.

Weg eins: Klartext oder Mapping im Client-Build aufspüren

Am einfachsten ist der Fall, in dem die Klartext-Query nie verborgen wurde.

So arbeitet etwa das Hot-Rank-System einer chinesischen Kurzvideo-Plattform: ein einzelner /graphql-Endpunkt, dazu der übliche Request-Body aus {operationName, variables, query}. Im Feld query steht die vollständige GraphQL-Query im Klartext, und operationName ist ein lesbarer Name wie hotRankQuery. Hier muss nichts erst beschafft werden: Ein Mitschnitt enthält bereits alles. Persisted Operations kommen dort nicht zum Einsatz – einfacher geht es kaum.

Etwas aufwendiger wird es, wenn Persisted Operations verwendet werden, der Client das Mapping aber weiterhin mitliefert. Um einen Hash senden zu können, muss der Client wissen, welche Operation welchem Hash entspricht. Diese operationName-zu-Hash-Tabelle – teils zusammen mit der Klartext-Query – steckt häufig im Frontend-Bundle. Build-Tools erzeugen sie oft als Manifest-Datei oder betten sie in ein Modul ein. Wer sie findet, hat Hash und Klartext auf einmal und kann danach Felder ergänzen oder das Selection Set ändern.

Schwierig ist nicht das Finden als solches, sondern die Größenordnung: Zehntausende Zeilen minifizierter, obfuskierter Code; dazu möglicherweise aufgeteilte und inline eingebettete Mappings. Genau hier kann ein Modell sinnvoll Zeit sparen. Es geht dabei nicht um Schlussfolgern, sondern um Chunk-basiertes Retrieval.

Der Test für Weg eins ist simpel: Wenn Klartext oder Mapping irgendwo im Client liegen könnten, sollte man zunächst zehn Minuten investieren, um sie zu finden. Gelingt das, ist dies der stärkste Weg – inklusive voller Kontrolle über den Endpunkt.

Weg zwei: Nicht nach Klartext suchen, sondern die Operation als Black Box wiederholen

Oft liegt die Klartext-Query allerdings gar nicht im Client vor.

Bei sauber umgesetzten Persisted Operations kennt der Client nur den Hash; die Klartext-Query liegt ausschließlich in der Server-Registry. Man kann das Bundle komplett auseinandernehmen und wird trotzdem nichts finden, weil die gesuchte Information nie ausgeliefert wurde. Wer in diesem Fall an Weg eins festhält, sucht nach etwas, das nicht existiert.

Weg zwei wird häufig unterschätzt: Die Klartext-Query ist nicht zwingend nötig. Benötigt wird meist die Antwort, nicht der Feldbaum der Query. Also zeichnet man den Operations-Identifier – Hash oder operationName – und den Variablen-Envelope auf, sendet beides unverändert erneut und ersetzt nur die relevanten Eingabeparameter. Welche Felder konkret selektiert werden, bleibt unbekannt; der Server liefert sie trotzdem zurück. Für die große Mehrheit von Datenabrufen und Monitoring-Aufgaben reicht das aus.

YouTubes innertube ist die Standardform dieses Ansatzes. Es handelt sich nicht einmal um GraphQL, sondern um feste, selbsterklärende Endpunkte wie youtubei/v1/{player,search,next}. Der Request-Body besteht aus einem context-Envelope mit Client-Typ und Version sowie einem Satz Parameter. Niemand versucht, YouTubes internen Query-Graphen zu „rekonstruieren“ – das wäre weder möglich noch sinnvoll. Der praktikable Weg ist, Client-Version und Context einmal aus den aktuellen Seitenressourcen auszulesen und diesen Envelope bei jedem Request unverändert mitzuführen. Ausgetauscht werden nur Eingaben wie videoId oder ein Suchbegriff, der feste Endpunkt bleibt derselbe. Die Semantik der Operation bleibt durchgehend eine Black Box. Der Fall, dass ein relevanter Wert nicht im statischen Code steckt, sondern aus Laufzeitressourcen gelesen werden muss, ist eine eigene Klasse von Reverse-Engineering-Problemen und wird separat behandelt.

Der Vorteil von Weg zwei: Er funktioniert unabhängig davon, ob die Klartext-Query im Client vorhanden ist. Ob Hash oder undurchsichtiger Envelope – man versucht nicht, ihn zu verstehen, sondern reproduziert ihn korrekt. Der Preis dafür: Man bleibt auf die Requests beschränkt, die der Client ohnehin sendet. Ein Feld, das der Client nie anfragt, lässt sich per Black-Box-Replay nicht beschaffen.

Es gibt noch einen Fall, der Replay verhindert: Der Envelope enthält ein Signaturfeld, das für jeden Request neu berechnet wird und abläuft. Dort endet die Black Box. Dann muss dieses eine Feld gesondert analysiert werden; welcher Signatur-Algorithmusfamilie es angehört, behandelt ein weiterer Artikel.

Wo die Plattformfälle einzuordnen sind

Stellt man die beiden Praxisfälle gegenüber, werden Einordnung und Begründung unmittelbar klar:

Plattformfall

Request-Form

Klartext im Client?

Passender Weg

Warum

GraphQL-Hot-Rank einer Kurzvideo-Plattform

/graphql + {operationName, variables, query}

Ja, Klartext direkt im Request-Body

Weg eins (nahezu ohne Aufwand)

Keine Persisted Operations, lesbarer operationName, Klartext-Query; ein Mitschnitt enthält alles

YouTube innertube

Feste Endpunkte + context-Envelope

Keine Klartext-Query im eigentlichen Sinn

Weg zwei (Black-Box-Replay)

Kein GraphQL, keine Query zu rekonstruieren; Context-Envelope einmal auslesen und unverändert übernehmen

Der Kontrast zwischen beiden Extremen zeigt vor allem eines: Die Vorgehensweise ist keine freie Präferenz, sondern folgt aus dem API-Design der Plattform. Im ersten Fall liegt Klartext offen vor; Missbrauch wird dort auf andere Weise begrenzt. Im zweiten wurde das „was soll abgefragt werden?“ in einen undurchsichtigen Envelope verlagert. Es gibt keine Klartext-Query zu suchen, also bleibt nur Replay.

Dazwischen liegt der relevante Bereich echter Persisted-GraphQL-Implementierungen. Die Klartext-Query kann im Client stecken – etwa als Mapping im Bundle, dann gilt Weg eins – oder ausschließlich auf dem Server liegen, während der Client nur den Hash besitzt, dann gilt Weg zwei. Diese Einordnung muss vor der eigentlichen Arbeit erfolgen.

Die entscheidende Frage: Brauche ich den Klartext überhaupt?

Der Unterschied im Aufwand um eine Größenordnung hängt vollständig an dieser einen Einschätzung.

Wenn eine Plattform tatsächlich nur den Hash ausliefert und die Klartext-Query auf dem Server hält, kann man tagelang das Bundle durchsuchen und am Ende feststellen, dass das Gesuchte nie ausgeliefert wurde. Das ist kein Problem mangelnder Ausdauer, sondern eine falsche Richtung. Mehr Aufwand führt nicht zum Ergebnis.

Umgekehrt hilft Black-Box-Replay nicht weiter, wenn die Query verändert werden muss, etwa um ein Feld abzurufen, das der Client nie anfordert. Dann führt kein Weg an Weg eins und der Klartext-Query vorbei – und wenn sie nicht beschaffbar ist, steckt man fest.

Die erste Frage sollte daher nicht lauten: „Wie bekomme ich die Query?“, sondern: Brauche ich den Klartext überhaupt?

  • Nur einen bestehenden Client-Request reproduzieren und seine Antwort auslesen: Weg zwei, also Black-Box-Replay. Das ist am günstigsten, wird am häufigsten übersehen und funktioniert unabhängig davon, ob Klartext verfügbar ist. Das sollte der Standard sein.

  • Das Selection Set ändern oder eine Query bauen, die der Client nie sendet: Weg eins, also die Klartext-Query beschaffen, ist zwingend. Wie teuer das wird, hängt davon ab, ob der Client das Mapping ausgeliefert hat. Falls nicht, entsteht der Aufwandssprung um eine Größenordnung. Dann muss man ihn akzeptieren oder noch einmal prüfen, ob die Query wirklich geändert werden muss.

Wer diese Entscheidung an den Anfang stellt, vermeidet den typischen Fehler: drei Tage lang Weg eins verfolgen und erst danach erkennen, dass Weg zwei gereicht hätte. Der Zielkonflikt zwischen „neu schreiben“ und „Black Box akzeptieren“ ist Thema des Artikels zur Purification Ladder; hier bestimmt er schlicht die Wahl des richtigen Beschaffungswegs.

Mappings finden ist Retrieval, keine Denkarbeit

Der technisch anspruchsvollste Schritt von Weg eins besteht darin, Operationsdeklaration und Mapping in einem Frontend-Build mit Zehntausenden Zeilen aufzuspüren. Genau dort spart ein Modell real Zeit. Wichtig ist aber, die Aufgabe korrekt einzuordnen.

Es ist keine Reasoning-Aufgabe. Das Modell muss nicht erklären, was der Code berechnet. Es muss in einer großen Textmenge lokalisieren, welcher Block das operationName-zu-Hash-Mapping deklariert, welches Modul die Klartext-Query inline enthält oder wo der Context-Envelope zusammengestellt wird. Das ist Chunk-basiertes Retrieval. Entscheidend sind genügend Kontext auf einmal und präzise Verweise innerhalb dieses Kontexts, nicht die Fähigkeit, lange Argumentationsketten zu führen.

Vorher kommt der mechanische Aufteilungsschritt: Das Build per Script in Module zerlegen, indexieren sowie Polyfills und irrelevante Fachmodule herausfiltern. Übergibt man dem Modell anschließend das Ergebnis, wird aus der Aufgabe schlicht: „Finde die Deklaration in diesen Blöcken.“

Die vier Stufen lassen sich in diesem Ablauf klar nach Fähigkeit trennen:

Schritt

Benötigte Fähigkeit

Auswahl

model id

Mapping / Operationsdeklaration in Zehntausenden Zeilen finden

Langer Kontext, verarbeitet große Build-Abschnitte und verweist präzise

Kimi K3

kimi-k3

Zwischen Weg eins und Weg zwei entscheiden; aus wenigen Samples ableiten, welche Envelope-Felder variieren

Starkes Reasoning, erkennt Strukturen und bewertet den Trade-off

Claude Opus 5

claude-opus-5

Hunderte Operationen stapelweise labeln, Replay-Stubs erzeugen, Variablentypen ergänzen

Günstig, hohe Parallelität

Claude Sonnet 5

claude-sonnet-5

Wenn Replay nicht verbindet: Diff lesen und Ursache zuordnen (fehlendes Context-Feld? geänderte Hash-Version?)

Mittleres Reasoning, erklärt anhand der Antwortdifferenz

GPT-5.6 Sol

gpt-5.6-sol

Die erste Stufe ist das Kernthema dieses Artikels. Ein Modellwechsel beim Lokalisieren verändert das Ergebnis sichtbar, weil hier das Kontextfenster die Grenze setzt. Ein Build umfasst Zehntausende Zeilen. Ein Modell mit kurzem Kontext muss abschneiden; dabei kann genau das Mapping verloren gehen. Wenn es danach meldet, nichts gefunden zu haben, liegt das nicht daran, dass es nicht suchen kann, sondern daran, dass es den relevanten Abschnitt nie gesehen hat.

Der Unterschied lässt sich einfach selbst testen:

  1. Einen echten Request aufzeichnen und Operations-Identifier – operationName oder Hash – sowie Variablen speichern.

  2. Das Frontend-Build per Script in Chunks teilen, zusammen mit dem Identifier an kimi-k3 übergeben und nach der Stelle fragen, an der die Operation deklariert wird und an der die passende Klartext-Query oder das Hash-Mapping liegt.

  3. Nur eines prüfen: Springt das Modell direkt zur richtigen Zeile? Treffer, Fehlschlag oder eine nahegelegene, aber falsche Stelle.

  4. Als Kontrolle denselben Input an ein Modell mit kurzem Kontext geben und prüfen, ob es scheitert, weil nicht alles hineinpasst. Die Trefferquote ist das Auswahlkriterium.

Ein Durchlauf genügt, um zu sehen: Langer Kontext ist bei dieser Aufgabe nicht bloß „etwas besser“, sondern entscheidet zwischen möglich und nicht möglich.

Das Umschalten zwischen Modellen ist der eigentliche Bremsklotz

Vier Modelle von drei Anbietern bedeuten drei SDKs, drei Authentifizierungsschemata und drei Fehlerformate. Wer für Retrieval, Bewertung, Massenverarbeitung und Attribution jeweils ein anderes Modell verwenden will, müsste naiv betrachtet alle drei Clients anbinden. Viele rechnen den Aufwand durch, halten ihn für nicht lohnend und bleiben durchgehend bei einem Modell. Dann wird ausgerechnet für das Long-Context-Retrieval eine Stufe genutzt, die das Build nicht aufnehmen kann – und anschließend heißt es, „das Modell findet es nicht“.

AIReiter vereinheitlicht diese Schicht: ein Key, ein OpenAI-kompatibles Interface, alle vier Stufen dahinter. Umzuschalten genügt es, das Feld model im Request-Body zu ändern.

# 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"

Wer bereits das OpenAI SDK nutzt, setzt base_url auf https://aireiter.com/api/v1 und muss sonst nichts ändern. Mit dem Anthropic SDK wird POST /api/v1/messages mit demselben Key aufgerufen.

Preislich laufen Claude-Modelle mit 30 % Rabatt auf den Listenpreis, GPT-Modelle zum halben Preis, und Kimi K3 ist über denselben Key erreichbar. Die Kosten dieses Workflows entstehen vor allem an zwei Stellen: beim Lokalisieren in Weg eins, wenn einige Hunderttausend Tokens des gechunkten Builds pro Input an kimi-k3 gehen; und beim Labeln Hunderter Operationen sowie beim massenhaften Erzeugen von Replay-Stubs, dem call-intensivsten Schritt, mit claude-sonnet-5 bei 30 % Rabatt. Der Rabatt greift damit genau bei dem Batch-Schritt mit der höchsten Aufrufdichte.

  • API-Key holen

  • Ohne Anmeldung ausprobieren: Zuerst einen Build-Chunk manuell eingeben und prüfen, ob die Long-Context-Stufe das Mapping in einem Durchlauf findet. Danach lässt sich entscheiden, ob sich die Integration lohnt.

Fazit

Eine GraphQL-API ohne Dokumentation bedeutet nicht, dass sie sich nicht integrieren lässt. Persisted Operations haben lediglich das „was soll abgefragt werden?“ aus dem Request verlagert – entweder in das Client-Build, wo es mit Weg eins auffindbar ist, oder ausschließlich auf den Server. Im zweiten Fall verfolgt man keinen Klartext, sondern spielt die Operation als Black Box über Weg zwei erneut ab.

Zwischen beiden Wegen liegt beim Aufwand eine Größenordnung. Ausschlaggebend ist nicht, welcher Ansatz vollständiger wirkt, sondern die Antwort auf zwei frühere Fragen: Liegt der Klartext im Client vor, und muss die Query geändert werden? Wer das vorab klärt, spart sich den Großteil unnötiger Arbeit.

Die Rolle des Modells ist dabei klar umrissen: Das Auffinden einer Deklaration in Zehntausenden Zeilen ist in Weg eins reines Retrieval. Eine Long-Context-Stufe verarbeitet das Material in einem Durchlauf und verweist präzise auf die relevante Stelle – aus Tagen manueller Suche werden Minuten. Die Entscheidung für den richtigen Weg nimmt sie nicht ab; diese Einschätzung sollte nach diesem Artikel bereits stehen. Sie übernimmt lediglich die Fleißarbeit beim Auffinden des Mappings.