Zum Hauptinhalt springen

Fehler und Limits

Zwei Arten von Fehlern​

Eine GraphQL-Anfrage kann an zwei Stellen scheitern, und die beiden sehen völlig unterschiedlich aus:

TransportfehlerAbfragefehler
WannBevor die GraphQL-Engine läuftBeim Parsen, Validieren oder Ausführen des Dokuments
StatusHTTP 4xx / 5xxHTTP 200
BodyProblemdokument nach RFC 9457GraphQL-Envelope mit einem errors-Array
BeispieleFehlendes oder abgelaufenes Token, fehlendes Recht, fehlerhaftes JSON, query über 10.000 ZeichenUnbekanntes 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
messageLesbarer Text. Für Entwicklerinnen und Entwickler gedacht und kein Vertrag — werten Sie ihn nicht aus.
locationsWo im Abfragedokument das Problem liegt.
pathDer Antwortpfad des Feldes, das gescheitert ist.
extensions.codeDie stabile Kennung. Werten Sie diese aus.
extensions.titleKurzbezeichnung des Problemtyps.
extensions.dataDetails zum Einzelfall — der gemessene Wert, die abgelehnte Eingabe.
extensions.errorsTeilfehler, wenn ein Fehler mehrere Bestandteile hat.
extensions.classificationDie 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.codeWird ausgelöst, wenndata enthält
graphql/query-too-deepDas Dokument mehr Relay-Ebenen verschachtelt als erlaubtdepth, limit
graphql/query-too-complexDie geschätzte Datensatzzahl der Abfrage das Budget übersteigtcost, limit
graphql/filter-too-deepEin where-Argument zu viele Ebenen verschachteltdepth, limit
graphql/invalid-cursorEin Cursor nicht von dieser API erzeugt wurdecursor
graphql/invalid-paginationPaginierungsargumente sich widersprechen oder negativ sinddie widersprüchlichen Argumente

Alles andere verwendet einen Typ aus dem allgemeinen Katalog:

extensions.codeWird ausgelöst, wenn
validation-failedEin Filter-Operand abgelehnt wird — zu viele _like-Wildcards, ein zu großer oder fehlerhafter JSON-Operand. Das betroffene Feld steht unter extensions.errors.
unknown-errorEin 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​

LimitWertDurchsetzung
Länge des Abfragedokuments10.000 ZeichenHTTP 422 mit Problemdokument
Abfragetiefe12 Relay-Ebenengraphql/query-too-deep
Abfragekosten100.000 geschätzte Datensätzegraphql/query-too-complex
Seitengrößemax. 100; Standard 20 Wurzel, 10 verschachteltStillschweigend gekappt
Verschachtelung von where4 Ebenengraphql/filter-too-deep
Verschachtelung von Objekten (Filter und order_by)3 EbenenDurch die Definition selbst begrenzt — ein tieferer Pfad ist im Schema nicht ausdrückbar
Operand von _contains / _contained_in3.000 Zeichenvalidation-failed
%-Wildcards in _like / _ilike2validation-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), wobei Seitengröße Ihr first/last ist, 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 —

cost-worked-example.graphql
{
equipment(first: 2, order_by: [{ name: asc }]) {
totalCount
pageInfo { hasNextPage endCursor }
edges { cursor node { id name asset_tag operational } }
}
}

— erreicht also 23:

SelektionKosten
totalCount1
pageInfo { hasNextPage endCursor }1 + 2 = 3
node { id name asset_tag operational }1 + 4 = 5
edges { cursor node }1 + 1 + 5 = 7
equipment-Connection1 + 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.