Fehler und Limits
Zwei Arten von Fehlern
Eine GraphQL-Anfrage kann an zwei Stellen scheitern, und die beiden sehen völlig unterschiedlich aus:
| Transportfehler | Abfragefehler | |
|---|---|---|
| Wann | Bevor die GraphQL-Engine läuft | Beim Parsen, Validieren oder Ausführen des Dokuments |
| Status | HTTP 4xx / 5xx | HTTP 200 |
| Body | Problemdokument nach RFC 9457 | GraphQL-Envelope mit einem errors-Array |
| Beispiele | Fehlendes oder abgelaufenes Token, fehlendes Recht, fehlerhaftes JSON, query über 10.000 Zeichen | Unbekanntes Feld, falscher Argumenttyp, abgelehnter Filter, überschrittenes Limit |
Schließen Sie niemals allein vom Statuscode auf Erfolg. Ein Abfragefehler ist eine 200. Prüfen Sie
immer, ob errors vorhanden ist.
Aufbau der Antwort
{
"data": null,
"errors": [
{
"message": "Human-readable explanation.",
"locations": [{ "line": 1, "column": 3 }],
"path": ["equipment"],
"extensions": {
"code": "https://problems.tourfold.com/graphql/invalid-pagination",
"title": "Invalid pagination arguments",
"data": { "first": 5, "last": 5 },
"classification": "DataFetchingException"
}
}
],
"extensions": {
"cost": { "estimated_rows": 23, "limit": 100000 }
}
}
Spätere Beispiele auf dieser Seite lassen classification aus Platzgründen weg; vorhanden ist es bei
jedem Fehler.
| Feld | |
|---|---|
message | Lesbarer Text. Für Entwicklerinnen und Entwickler gedacht und kein Vertrag — werten Sie ihn nicht aus. |
locations | Wo im Abfragedokument das Problem liegt. |
path | Der Antwortpfad des Feldes, das gescheitert ist. |
extensions.code | Die stabile Kennung. Werten Sie diese aus. |
extensions.title | Kurzbezeichnung des Problemtyps. |
extensions.data | Details zum Einzelfall — der gemessene Wert, die abgelehnte Eingabe. |
extensions.errors | Teilfehler, wenn ein Fehler mehrere Bestandteile hat. |
extensions.classification | Die eigene grobe Kategorie der GraphQL-Engine (ValidationError, DataFetchingException). Nur informativ und kein Bestandteil des Vertrags dieser API — sie gehört zur Bibliothek und kann sich mit einem Upgrade ändern. Werten Sie code aus. |
extensions.code ist eine Typ-URI und dieselbe Zeichenkette, die ein REST-Fehler im Feld type trägt.
Eine Kennung deckt beide Oberflächen ab; ein Client, der die REST-Fehler von Tourfold schon verarbeitet,
braucht kein zweites Vokabular. Ein umschließendes Problem-Objekt gibt es bewusst nicht — title und data
stehen flach neben code.
Ein Feld status gibt es nicht. Die Antwort ist immer HTTP 200, es gibt also keinen Status zu melden.
Wie weit ein Fehler durchschlägt
GraphQL ersetzt ein gescheitertes Feld durch null und nennt den Grund in errors. Wie viel sonst
erhalten bleibt, hängt davon ab, ob dieses null an der betreffenden Stelle erlaubt ist — und bei dieser
API lautet die Antwort meist „nicht viel“:
Eine gescheiterte Wurzel-Connection setzt das gesamte data auf null. Wurzelfelder sind non-null
(equipmentConnection!); ein null ist dort also nicht zulässig und der Fehler schlägt auf das umgebende
Objekt durch — und das ist data selbst. Alle Beispiele auf dieser Seite zeigen deshalb "data": null und
nicht "data": { "equipment": null }.
Eine gescheiterte verschachtelte Relation setzt nur dieses Feld auf null. To-many- und
To-one-Relationsfelder sind nullable, ein Fehler darin bleibt also lokal und die Nachbarfelder behalten
ihre Daten. Genau dort sehen Sie tatsächlich eine Antwort mit data und errors zugleich.
Ein ungültiges Dokument führt nichts aus. Ein unbekanntes Feld oder ein falsch typisiertes Argument
wird vor der Ausführung erkannt — und dann fehlt der Schlüssel data vollständig, er ist nicht null.
Es wurde nichts ausgeführt, und „keine Daten“ ist eine andere Aussage als „die Daten sind null“. Alle
Probleme des Dokuments werden auf einmal gemeldet.
Schreiben Sie also keinen Client, der davon ausgeht, dass immer Teildaten verfügbar sind. Prüfen Sie zuerst
errors und behandeln Sie data als möglicherweise null und möglicherweise nicht vorhanden.
Nicht jeder Fehler hat einen Code
Fehler, die die GraphQL-Engine auslöst, bevor Tourfold-Code läuft — Syntaxfehler, unbekannte Felder,
Typkonflikte —, tragen kein extensions.code. Mehr als message und locations gibt es dort nicht.
Für Clients gilt deshalb: Werten Sie extensions.code aus, wenn es vorhanden ist, weichen Sie sonst auf
message aus, und gehen Sie nie davon aus, dass jeder Eintrag in errors einen Code hat.
{
"errors": [
{
"message": "Validation error (FieldUndefined@[equipment/cpu_processor]) : Field 'cpu_processor' in type 'equipment' is undefined",
"locations": [{ "line": 4, "column": 5 }],
"extensions": { "classification": "ValidationError" }
}
]
}
Beachten Sie, was fehlt: kein data-Schlüssel und kein code. classification ist die eigene Kategorie
der Engine und kein Tourfold-Problemtyp.
Prüfen Sie über den Schema-Endpoint, welche Felder Ihr Workspace tatsächlich hat — und beachten Sie, dass ein fehlendes Feld auch eine Namenskollision statt eines Tippfehlers sein kann.
Fehlerkatalog
Fehler, die es nur in GraphQL gibt, tragen den Namensraum graphql/:
extensions.code | Wird ausgelöst, wenn | data enthält |
|---|---|---|
graphql/query-too-deep | Das Dokument mehr Relay-Ebenen verschachtelt als erlaubt | depth, limit |
graphql/query-too-complex | Die geschätzte Datensatzzahl der Abfrage das Budget übersteigt | cost, limit |
graphql/filter-too-deep | Ein where-Argument zu viele Ebenen verschachtelt | depth, limit |
graphql/invalid-cursor | Ein Cursor nicht von dieser API erzeugt wurde | cursor |
graphql/invalid-pagination | Paginierungsargumente sich widersprechen oder negativ sind | die widersprüchlichen Argumente |
Alles andere verwendet einen Typ aus dem allgemeinen Katalog:
extensions.code | Wird ausgelöst, wenn |
|---|---|
validation-failed | Ein Filter-Operand abgelehnt wird — zu viele _like-Wildcards, ein zu großer oder fehlerhafter JSON-Operand. Das betroffene Feld steht unter extensions.errors. |
unknown-error | Ein unerwarteter serverseitiger Fehler auftritt. Die Meldung ist bewusst allgemein; die Details werden serverseitig protokolliert. |
Teilfehler
Hat ein Fehler mehrere Bestandteile, erscheinen sie unter extensions.errors. Ein Teilfehler trägt nur, was
pro Vorkommen variiert — code, message, path, data — und verschachtelt sich nicht weiter. path ist
ein Feldpfad, so wie GraphQL alles andere adressiert; eine Antwort mischt also nie Feldpfade mit
JSON-Pointern. Bei der Umwandlung in ein REST-Problemdokument wird daraus ein Pointer nach RFC 6901.
Limits
| Limit | Wert | Durchsetzung |
|---|---|---|
| Länge des Abfragedokuments | 10.000 Zeichen | HTTP 422 mit Problemdokument |
| Abfragetiefe | 12 Relay-Ebenen | graphql/query-too-deep |
| Abfragekosten | 100.000 geschätzte Datensätze | graphql/query-too-complex |
| Seitengröße | max. 100; Standard 20 Wurzel, 10 verschachtelt | Stillschweigend gekappt |
Verschachtelung von where | 4 Ebenen | graphql/filter-too-deep |
Verschachtelung von Objekten (Filter und order_by) | 3 Ebenen | Durch die Definition selbst begrenzt — ein tieferer Pfad ist im Schema nicht ausdrückbar |
Operand von _contains / _contained_in | 3.000 Zeichen | validation-failed |
%-Wildcards in _like / _ilike | 2 | validation-failed |
Abfragetiefe und Filterverschachtelung sind unabhängige Regler mit unterschiedlichen Werten: Der eine
begrenzt die Relationsnavigation, der andere einen where-Baum. Den einen zu erhöhen wirkt nicht auf den
anderen.
Wie die Abfragetiefe gezählt wird
Gezählt wird in Selektionsebenen, nicht in Relationssprüngen — deshalb ist 12 weniger, als es klingt:
- Eine einfache Connection kostet bereits 4: Wurzelfeld →
edges→node→ Blattfeld. - Jeder weitere To-many-Sprung kostet 3: Relationsfeld →
edges→node. - Jeder To-one-Sprung kostet 1.
Eine Abfrage mit einem Sprung hat damit Tiefe 7, eine mit drei Sprüngen Tiefe 13 — also über dem Limit. Teilen Sie eine tiefe Traversierung in mehrere Abfragen auf oder beginnen Sie am anderen Ende der Relation.
{
"data": null,
"errors": [
{
"message": "Query depth 15 exceeds the maximum of 12. Each to-many hop costs three levels (field, edges, node); select fewer levels or split the traversal into separate queries.",
"extensions": {
"code": "https://problems.tourfold.com/graphql/query-too-deep",
"title": "Query too deep",
"data": { "depth": 15, "limit": 12 }
}
}
]
}
Abfragekosten
Eine Antwort meldet die Kosten der Abfrage, sofern die Anfrage weit genug kam, um analysiert zu werden:
"extensions": { "cost": { "estimated_rows": 23, "limit": 100000 } }
Vorhanden: bei erfolgreicher Ausführung und bei einer Ablehnung mit query-too-complex — was der Sinn
der Sache ist, denn so erkennen Sie, wie weit über dem Budget Sie lagen.
Nicht vorhanden: wenn die Anfrage die Kostenanalyse nie erreicht hat. Ein Syntax- oder
Validierungsfehler wird vorher erkannt, und es gibt keine ausführbare Operation zu bewerten. Auch eine
Ablehnung mit query-too-deep liegt davor, weil die Tiefenprüfung zuerst läuft. Behandeln Sie
extensions.cost als optional und setzen Sie sein Vorhandensein nie voraus.
estimated_rows misst, wie weit die Antwort anwachsen kann — wie viele Knoten die Abfrage erzeugen
lassen darf. Es ist keine Obergrenze für gelesene Datenbankzeilen, ausgeführte Statements oder aufgewendete
Zeit: Ein Teil der internen Arbeit (das Auflösen eines Relationsfilters oder einer verschachtelten Relation
je übergeordnetem Datensatz) verhält sich nicht proportional zur Antwort und wird hier nicht verrechnet.
Nutzen Sie den Wert, um Antwortgrößen im Rahmen zu halten, nicht als Leistungsgarantie.
Bewertet werden Datensätze, nicht Felder:
- Ein einfaches Feld kostet
1 + (seine Kinder). - Eine Connection kostet
1 + Seitengröße × (ihre Kinder), wobeiSeitengrößeIhrfirst/lastist, gekappt auf 100, oder der jeweils geltende Standardwert, wenn Sie keines von beiden angeben.
Die Multiplikation ist der entscheidende Punkt: Eine Connection bezahlt ihren gesamten Teilbaum einmal pro Datensatz. Diese Abfrage —
{
equipment(first: 2, order_by: [{ name: asc }]) {
totalCount
pageInfo { hasNextPage endCursor }
edges { cursor node { id name asset_tag operational } }
}
}
— erreicht also 23:
| Selektion | Kosten |
|---|---|
totalCount | 1 |
pageInfo { hasNextPage endCursor } | 1 + 2 = 3 |
node { id name asset_tag operational } | 1 + 4 = 5 |
edges { cursor node } | 1 + 1 + 5 = 7 |
equipment-Connection | 1 + 2 × (1 + 3 + 7) = 23 |
Daraus folgen zwei Dinge, und sie sind das Gegenteil dessen, was ein Feldzähler-Budget nahelegen würde:
- Eine breite Seite ist günstig. 20 Spalten einer Seite mit 25 Datensätzen sind einige hundert Datensätze.
- Eine schmale, tiefe Traversierung ist teuer. Eine Spalte, 100 × 100 × 100 paginiert, sind eine Million Datensätze und wird abgelehnt — obwohl das Dokument kaum eine Handvoll Felder nennt.
Wird eine Abfrage abgelehnt, ist die günstigste Korrektur fast immer ein kleineres first an der
innersten Connection, denn dieser Faktor wird am häufigsten multipliziert.
{
"data": null,
"errors": [
{
"message": "Query is estimated to materialise 3003001 rows, above the budget of 100000. A connection multiplies its subtree by its page size — request smaller pages or select fewer nested fields.",
"extensions": {
"code": "https://problems.tourfold.com/graphql/query-too-complex",
"title": "Query too complex",
"data": { "cost": 3003001, "limit": 100000 }
}
}
]
}
Das Kostenlimit gilt pro Abfrage. Es ist kein Budget über die Zeit; dass eine einzelne Anfrage darunter bleibt, ist also für sich kein Grund, Anfragen so schnell wie möglich zu stellen — halten Sie die Anfragerate an dem aus, was Ihre Integration tatsächlich braucht, und rechnen Sie bei dauerhaft hoher Last mit einer Begrenzung.