Skip to content

Error and log codes

GraphPilot reports what it did with a stable code rather than a sentence. This page lists the codes of the edge, the telemetry values recorded beside them, the GraphPilot API’s error codes and the CLI’s exit codes.

An edge code appears in three places, so a support thread can start from any of them:

  • on the response, as gp-cache-reason for the cache codes, or in the error body for the codes that reject a request,
  • in your logs: a decision or refusal is recorded on the request itself and listed under Edge codes at the end of the request’s Overview tab in Insights, and a warning or error is one of the log lines, read through the API’s Service.logs (both stored under the same rules as telemetry, below),
  • in your request telemetry, which the portal’s Insights view reads, stored under the rules on what Insights needs.

Some codes refine another one. REQUEST_BODY_TOO_LARGE refines PAYLOAD_TOO_LARGE: one request can log both, as a cause and its outcome, and they share a request_id. A view that wants a single line per event can drop the parent and keep the child.

A refining code is for your logs, not for the caller. The client is told the parent, because its situation and its fix are the same whichever of the two ceilings answered, and which one did is something only you can see. Do not switch on a refining code in client code: it never arrives there. The tables below mark each one log only.

Where a row below says what a code’s detail names (a header, a cookie, a client), that detail is shown beside the code: under Edge codes in the request’s Overview tab for a decision or refusal, or on the log line for a warning or error.

These end the request with the status shown. The caller receives the code in the body: for a GraphQL request, as a GraphQL error carrying it in extensions.code; for any other request, as an application/problem+json body carrying it in title. Rows marked log only appear in your logs and telemetry, while the caller is told the code they refine.

Your origin is not called for any of them, except BAD_GATEWAY, GATEWAY_TIMEOUT and the codes that refine them, which are produced after the request was sent on, to your origin or, with shielding, to the shield, and no usable answer came back.

CodeStatusRefinesMeaning
UNAUTHENTICATED401A credential GraphPilot could not accept.
AUTH_INVALID_TOKEN401UNAUTHENTICATEDThe signature did not verify, the token expired, it carries no exp and the provider does not set allow_missing_exp, or a required claim did not match.
AUTH_UNKNOWN_KEY_ID401UNAUTHENTICATEDThe token names a key GraphPilot does not have, and refreshing did not produce it.
AUTH_KEY_MATERIAL_UNAVAILABLE401UNAUTHENTICATEDNo key material at all for this provider, affecting every request.
FORBIDDEN403The operation is not on this service’s allowlist.
INTROSPECTION_DISABLED403The service does not answer schema introspection. Does not apply to operations sent over a subscription socket.
METHOD_NOT_ALLOWED405The operation may not be run with the method the request used. The response carries an Allow header naming the method to use.
PAYLOAD_TOO_LARGE413The request body is over a limit this service sets, checked before the body is parsed. This is what the caller is told for either of the two rows below; the message carries no numbers, so a caller learns only that it sent too much.
REQUEST_BODY_TOO_LARGE413PAYLOAD_TOO_LARGELog only. The body was over request.max_body_bytes, the service-wide ceiling, checked before the client is even identified.
CLIENT_BODY_TOO_LARGE413PAYLOAD_TOO_LARGELog only. The body cleared the service-wide ceiling but was over the max_body set on the client profile that matched it. Its detail names the client.
UNSUPPORTED_GRAPHQL_BODY415The body sent to a GraphQL path is not a single operation GraphPilot can check (a JSON batch, a multipart upload, text, no content type, or JSON that names no operation), and the service does not forward that kind unchecked. See bodies on a GraphQL path.
INGRESS_UNSUPPORTED_GRAPHQL_BODY415UNSUPPORTED_GRAPHQL_BODYLog only. Names the kind of body that was refused (batch, multipart or other); listing that kind in request.forward_unchecked_bodies forwards such bodies to your origin unchecked.
INVALID_GRAPHQL_REQUEST400The request on a GraphQL path carries an operation GraphPilot cannot read unambiguously, so it is refused rather than forwarded unchecked: your origin might read it differently, and none of GraphPilot’s checks would then have seen what it runs. The message says what was wrong with the request’s form.
INGRESS_UNREADABLE_GRAPHQL_REQUEST400INVALID_GRAPHQL_REQUESTLog only. reason says what could not be read: duplicate_key (the JSON body repeats a key within one object), too_deep (the JSON body nests more than 128 levels), repeated_parameter (a GET repeats query, operationName, variables or extensions), unreadable_parameter (a GET’s variables or extensions is not JSON, repeats a key, or nests too deep), query_string_override (a POST, or a GET with a body, carries query, variables or extensions in its URL, or an operationName that differs from the body’s), or coded_body (the body carries a Content-Encoding such as gzip, deflate, br or zstd: a body on a GraphQL path must be sent uncompressed; identity counts as uncompressed). Refused whatever request.forward_unchecked_bodies lists.
NOT_A_GRAPHQL_PATH404The request carries a GraphQL operation on a path that is not one of this service’s GraphQL paths (request.graphql_paths), where the GraphQL checks do not run, so it was refused rather than forwarded unchecked. Sent while at least one of those checks is in force, which by default it is. The message names the GraphQL paths. A query that is not a GraphQL document, such as a search term, is never refused. See GraphQL on other paths.
INGRESS_OFF_PATH_GRAPHQL404NOT_A_GRAPHQL_PATHLog only. form says where the operation was found: json_body (a JSON body whose top-level query is a GraphQL document, or that carries extensions.persistedQuery), document_body (an application/graphql body), form_body (an application/x-www-form-urlencoded body) or query_string (the URL’s query or extensions parameter). protections lists the checks the operation would have gone around: introspection_block, trusted_documents, client_persisted_query_required, max_query_depth, max_document_bytes, max_selections. Point the client at a GraphQL path, or add this path to request.graphql_paths if your origin serves GraphQL there.
GRAPHQL_INVALID_QUERY400INVALID_GRAPHQL_REQUESTLog only. The GraphQL document could not be parsed. A leading byte-order mark is skipped first. A block string with a backslash directly before a quote is refused here too, because parsers disagree on where such a string ends.
GRAPHQL_EMPTY_QUERY400INVALID_GRAPHQL_REQUESTLog only. The operation selects no field your deployed schema defines.
GRAPHQL_AMBIGUOUS_OPERATION400INVALID_GRAPHQL_REQUESTLog only. The document defines several operations and the request named none, so none could be selected.
GRAPHQL_OPERATION_NOT_FOUND400INVALID_GRAPHQL_REQUESTLog only. The request named an operation the document does not define.
QUERY_DEPTH_LIMIT_EXCEEDED400The operation nests deeper than this service allows. The message gives the depth and the limit. Also returned, whatever request.max_query_depth says and even with it set to 0, for a document nested more than 128 levels deep, counted on its brackets before it is parsed or on its fields with fragments expanded: the ceiling GraphPilot keeps for itself so a document cannot exhaust its stack. Does not apply to operations sent over a subscription socket.
DOCUMENT_SIZE_LIMIT_EXCEEDED400The operation’s document is longer than this service allows (request.max_document_bytes, or a lower client profile value). The message gives the length and the limit. See query document limits. Does not apply to operations sent over a subscription socket.
DOCUMENT_SELECTION_LIMIT_EXCEEDED400The operation makes more selections than this service allows (request.max_selections, or a lower client profile value), counted with fragment spreads expanded. The message gives the count and the limit. Does not apply to operations sent over a subscription socket.
RESERVED_FIELD_PREFIX400The document uses an alias, or a field name without an alias, that starts with origin.field_prefix (__gp_ by default). GraphPilot names the fields it adds with that prefix and removes everything carrying it from the response, so a key of your own there would disappear from the answer. Every operation and fragment in the document is checked, including ones the request does not run. The message names the key and the prefix; rename the alias. An empty field_prefix turns the check off. See selection set expansion.
UNKNOWN_CLIENT403The request matched no client profile this service declares, and client.unknown is "reject".
CLIENT_VERSION_UNSUPPORTED403The client is declared, but the version it reported is below that profile’s min_version. The required version is in the message.
PERSISTED_QUERY_REQUIRED403The client’s profile sets persisted_query = "required" and the payload was not a hash on its own. A document is refused even when a hash travels beside it, which is what an APQ registration looks like. Does not apply to operations sent over a subscription socket.
INTERNAL_SERVER_ERROR500GraphPilot failed to process the request. The message says nothing about why.
BAD_GATEWAY502The origin could not be reached or did not answer usably.
ORIGIN_RESPONSE_DECODED_TOO_LARGE502BAD_GATEWAYLog only. Your origin compressed its response although GraphPilot asked for an uncompressed one, and the body would decode to more than GraphPilot processes.
ORIGIN_RESPONSE_ENCODING_CORRUPT502BAD_GATEWAYLog only. Your origin compressed its response although GraphPilot asked for an uncompressed one, and the body did not decode under the coding its Content-Encoding named.
ORIGIN_RESPONSE_ENCODING_UNSUPPORTED502BAD_GATEWAYLog only. Your origin compressed its response with a coding GraphPilot cannot decode. It decodes gzip, deflate, br and zstd.
SHIELD_RESPONSE_UNDECODABLE502BAD_GATEWAYLog only. With shielding, the shield answered this edge location with a compressed body although the edge asked for an uncompressed one, and the edge could not decode it. reason says why (unsupported, corrupt or too_large). The edge location does not then call your origin itself.
SHIELD_UNAVAILABLE_NO_FALLBACK502Log only. The caller receives BAD_GATEWAY. With shielding, a mutation could not be sent through the shield, so this edge location answered 502 rather than send it to your origin itself, which could run it twice and would purge this location’s cache before the shield’s. reason says why, with the same values as SHIELD_UNAVAILABLE_FALLBACK. A request whose operation the edge cannot see (a persisted query hash without a document) is treated the same way when the failure came after the request may have left the edge location, since it may be a mutation. shield_result is UNAVAILABLE.
GATEWAY_TIMEOUT504With shielding, the request was sent to the shield and no usable response came back: the hop exceeded its time budget, or the connection dropped or the response was incomplete, oversized or malformed. Applies to queries and mutations alike. The edge location does not then call your origin itself, so your origin is never called twice for the same request. shield_result is TIMEOUT. See shielding and the origin connection.

A body sent to a GraphQL path (request.graphql_paths) is read as one operation: JSON carrying a query or a persisted query hash, or an application/graphql document, which is checked like any other operation. A JSON body that repeats a key or nests more than 128 levels deep, and a document that does not parse, are refused with 400 and INVALID_GRAPHQL_REQUEST. Any other body is refused with 415 and UNSUPPORTED_GRAPHQL_BODY before it reaches your origin, unless its kind is listed in request.forward_unchecked_bodies. See persisted queries. A GraphQL operation sent to any other path is refused with 404 and NOT_A_GRAPHQL_PATH; see GraphQL on other paths.

GraphPilot sends every origin request with Accept-Encoding: identity, because it has to read the response body to derive surrogate keys and strip the fields it injected. The edge compresses toward the client itself. An origin that compresses anyway costs a decode (ORIGIN_RESPONSE_DECODED, below) or, when decoding fails, a 502. See response headers.

See authentication, limits and origin timeouts for what each of these is protecting.

Every code here arrives on gp-cache-reason alongside gp-cache: PASS. MISS means the response was stored, so a response that was not stored never reports MISS, whatever the reason (see which status a refused store reports). The response is still delivered to the client in full; only storing was refused. One code in this group, CACHE_SKIPPED_NO_ORIGIN_RESPONSE, never reaches gp-cache-reason, because no response reaches the client on its path: it appears only in log lines and request telemetry. The order these are evaluated in is on when a response is cached.

CodeMeaningWhat to do
CACHE_SKIPPED_UNCACHEABLE_OPERATIONThe operation was a mutation or a subscription. Only a query is ever cached, whatever the origin’s Cache-Control said.Nothing. This is by design.
CACHE_SKIPPED_NO_CACHE_POLICYNeither an origin Cache-Control header nor a schema cache policy applied.Send Cache-Control from your origin, or annotate the schema with @cacheControl.
CACHE_SKIPPED_REQUEST_NO_STOREThe request’s own Cache-Control said no-store, so its response was not cached. The cache was still read.Nothing, unless the client should not send it. Set cache.request_directives.no_store = "ignore" to cache such responses anyway. See the client’s own Cache-Control.
CACHE_SKIPPED_ENTRY_NOT_REPLACEDThe cache already held an entry for this key that the request did not use, because it was older than the matching client profile’s max_age or could not be read, and the entry was not replaced. An unreadable entry is normally overwritten with the origin’s answer (reported as MISS); this code means the overwrite was not possible. The response was served from your origin without being stored, and the entry is unchanged.Nothing for a client with a max_age: this is what the cap does.
CACHE_SKIPPED_NO_ORIGIN_RESPONSEThe cache had no usable entry, but no origin response came back to store: the origin request failed, or GraphPilot answered the request itself before reaching the origin. Nothing was cached. Log and telemetry only, never on gp-cache-reason.Look at the request’s other codes: an origin failure (BAD_GATEWAY, a timeout) or a refusal explains it.
CACHE_SKIPPED_NO_MAX_AGEThe deciding policy stated no freshness: the origin response carried neither max-age nor s-maxage, or the schema policy resolved no positive maxAge. Checked before the discriminators, so it is also what a private response with no max-age reports.Add max-age or s-maxage at the origin, or a maxAge to every type the query selects.
CACHE_SKIPPED_NO_STOREThe origin response was marked no-store.Nothing, unless the marking is wrong.
CACHE_SKIPPED_NO_CACHE_DIRECTIVEThe origin response was marked no-cache, which forbids reuse without revalidating with the origin first.Use max-age to cache it, no-store not to.
CACHE_SKIPPED_ERROR_STATUSYour origin answered with a 4xx or 5xx status that cache.policy.store_error_statuses does not list, so the response was not stored, whatever its Cache-Control said.Nothing, unless the error is meant to be cached. To store one on purpose, such as a 404 for a resource that does not exist, list its status in cache.policy.store_error_statuses.
CACHE_SKIPPED_UNPARSEABLE_GRAPHQL_RESPONSEThe response to a GraphQL request was not JSON GraphPilot could parse, or was larger than it parses. It was delivered to the client unchanged but not stored, because neither GraphQL errors nor surrogate keys could be read from it.Check that your origin answers GraphQL requests with a JSON body.
CACHE_SKIPPED_GRAPHQL_ERRORSYour origin returned a GraphQL error. An errored response describes the moment it was produced rather than the data, and caching it would serve that failure to every later request for the same query until the entry expired.Nothing, unless the error is unexpected. Fix it at the origin.
CACHE_SKIPPED_PRIVATE_WITHOUT_DISCRIMINATORThe response is partitioned, by an origin private or by the schema’s scope for the query (also when the origin header decided the lifetime), but at least one discriminator it is keyed on was not available, so it could not be filed. On the schema path its detail separates names the request did not carry from names nothing configures. Also reported when the request carried a credential and a [[cache.policy.vary]] rule that partitions the response resolved to nothing or to the empty string; its detail names those rules under unresolved.For a private response, configure cache.policy.private and check that its expression resolves for this request. For a named scope, check that every name it lists has a [[cache.policy.vary]] entry whose expression resolves.
CACHE_SKIPPED_UNRESOLVED_SELECTIONThe query selected a field, or a fragment on a type, that the deployed schema does not declare, at any depth, usually because your origin was deployed before the schema was. Who may share that field’s value is unknown, so the response was not stored, whatever the origin’s Cache-Control said.Deploy the schema your origin runs. See deploy the schema before the origin.
CACHE_SKIPPED_CREDENTIALED_REQUESTThe request carried a credential (Authorization, a Cookie naming at least one cookie that is not on cache.cookies.allow, or the header an [auth.providers.<name>] block is configured to read), the origin never said the response may be held by a shared cache, and no discriminator identified the caller. Its detail names which header carried the credential, and for cookie also names the first blocking cookie, unless the header’s value carries no readable cookie name at all.Mark the response public or s-maxage if it is the same for everyone, configure a private policy, or add the cookie to cache.cookies.allow if it carries no session, as App Engine’s GAESA does by default. See authenticated requests.
CACHE_SKIPPED_SET_COOKIEThe response carried Set-Cookie or Set-Cookie2 setting at least one cookie that is not on cache.cookies.allow. A cache entry would re-assert that cookie on every hit until it expired. Its detail names the blocking cookie, not just the header.Stop setting cookies on reads, mark the response no-store, or add the cookie to cache.cookies.allow if it carries no session. This is the most common cause of a service that caches nothing.
CACHE_SKIPPED_AUTHENTICATION_HEADERThe response carried a header stating something about one caller’s session: an authentication challenge, Clear-Site-Data, or Set-Login.Send it only on the responses that need it.
CACHE_SKIPPED_VARY_WILDCARDThe response carried Vary: *, so no cache entry could legitimately match a later request.Name the headers you actually vary on.
CACHE_SKIPPED_NO_SURROGATE_KEYSThe response carried no surrogate keys.Rare. Check that the request reached the GraphQL layer at all.
CACHE_SKIPPED_SURROGATE_KEYS_TOO_LARGEThe surrogate keys exceeded the 16,000-byte cache header limit.Paginate the response. See how many keys fit on one response.
CACHE_SKIPPED_SERIALIZATION_FAILEDThe response could not be encoded for storage.Nothing. This is an internal fault, not a configuration problem.
CACHE_SKIPPED_INSERT_FAILEDThe cache storage refused the write.Nothing. This is an internal fault.
CACHE_SKIPPED_SHIELD_ENTRY_EXHAUSTEDWith shielding, the response came from the shield, which had already held it for its whole max-age, so no lifetime was left for a copy at this edge location. An entry never lives longer in total than your origin allowed.Nothing. This is by design.
CACHE_SKIPPED_SHIELD_RESPONSE_NOT_STORABLEWith shielding, the shield’s response was served but not stored at this edge location: it came back STALE or PASS, or its metadata or age could not be read. A stale or uncacheable response is never copied to every edge location.Look at the shield’s own reason for why the shield did not store it: the portal shows it beside this code. When shield_result is HIT or MISS, SHIELD_META_UNUSABLE on the same request says what could not be read.

A response with a 4xx or 5xx status is not stored by default, even when its Cache-Control permits it, and reports PASS with CACHE_SKIPPED_ERROR_STATUS. A status listed in cache.policy.store_error_statuses is stored under the ordinary rules instead. See an error status is not stored unless you opt in.

Reported alongside gp-cache: PASS, like the codes above. Most of these passes are decided before a lookup; CACHE_PASS_READ_DISABLED follows a lookup that failed. Either way the request went to your origin and its response was not stored.

CodeMeaning
CACHE_PASS_BYPASS_POLICYThe service’s cache bypass policy matched this request.
CACHE_PASS_UNCACHEABLE_METHODThe request method is listed in cache.policy.uncacheable_methods, which defaults to OPTIONS.
CACHE_PASS_WEBSOCKET_UPGRADEA WebSocket upgrade is never cacheable and always reaches the origin.
CACHE_PASS_MISSING_CACHE_KEYThe request produced no cache key. Also for a GraphQL request whose body carries a document together with a persisted query hash that is not that document’s hash: it is never looked up or stored, and runs as before.
CACHE_PASS_READ_DISABLEDThe cache could not be read, so the request went to the origin.
CACHE_PASS_CLIENT_CACHE_OFFThe client profile that matched this request sets cache = "off".
CACHE_PASS_REQUEST_NO_CACHEThe request’s own Cache-Control said no-cache or max-age=0, and cache.request_directives.no_cache is "honor". Off by default, because any caller can send it.

These are not problems. They appear in logs and telemetry, not as a gp-cache-reason.

CodeRefinesMeaning
CACHE_STOREDThe response was written to the cache.
CACHE_STORED_PRIVATECACHE_STOREDThe request carried a credential and the origin never said the response may be held by a shared cache, so the entry was partitioned under the private cache policy rather than shared.
CACHE_STORED_WITHOUT_STALE_WINDOWCACHE_STOREDThe origin response carried stale-while-revalidate together with must-revalidate or proxy-revalidate. Those forbid serving it once stale, so the entry was stored for its max-age only and will not be served STALE. Its detail carries the dropped window as stale_while_revalidate.
CACHE_POLICY_ORIGIN_HEADERYour origin sent a Cache-Control header, so it decided the response, outranking the schema’s @cacheControl policy. overrode_schema_policy says whether there was a schema policy to outrank, and cache_control carries the header verbatim.
CACHE_POLICY_ORIGIN_HEADER_IGNOREDYour origin sent a Cache-Control that constrains neither storage nor freshness (a bare private, for example), so it decided nothing and the schema’s @cacheControl policy governed instead. A private on such a header still partitions the entry. Turned off with cache.ignore_origin_header_without_freshness = false.
CACHE_POLICY_SCHEMAThe schema’s @cacheControl policy decided the response. max_age is the lowest across every field the query selected, and max_age_set_by names the fields that set it (only those that declared the value rather than inherited it, capped and marked and more when there were further ones). Not restored on a cache hit.
CACHE_PURGEDSurrogate keys were purged from the cache. See automatic invalidation.
ORIGIN_RETRIEDA failed origin request was sent again, per the configured retry policy.
SHIELD_UNAVAILABLE_FALLBACKWith shielding, the request could not be sent through the shield, or what came back was not a shield response, so this edge location went to the origin itself. A query falls back on any of these failures. A mutation never falls back: it is answered 502 instead (SHIELD_UNAVAILABLE_NO_FALLBACK), and so is a request whose operation the edge cannot see once it may have left this location. A failure after the shield may have started on the request is a 504 (GATEWAY_TIMEOUT) instead, for every request.
SHIELD_META_UNUSABLEWith shielding, the shield’s response was served to the client, but this edge could not read what it needs to store the response here (its metadata was missing, too large, or did not decode), so it was passed through without being cached at this location.
SHIELD_PURGE_NOT_CARRIEDWith shielding, the shield purged a mutation’s surrogate keys but could not tell this edge location which, so the edge did not purge them again on the way back. reason says why (too_large, unencodable or undecodable). This location’s copy of those entries is then cleared only when the shield’s purge reaches it.
SHIELD_HOP_REJECTEDA request arrived claiming to come from another edge location but did not verify as such, or arrived at a location other than the shield it names, so it is served as an ordinary request rather than as a hop from another edge location. When it came from another edge location, the shield records it as a hop entry rather than as a client request; like every request the shield runs, it counts at the shield toward request units.
ORIGIN_RESPONSE_DECODEDYour origin compressed its response although GraphPilot asked for an uncompressed one, so GraphPilot decoded it first. The response is keyed and cached as usual; encoded_bytes and decoded_bytes carry both sizes. Configure your origin to honor Accept-Encoding: identity to save the work.
INGRESS_RESERVED_HEADER_DROPPEDA header in the reserved gp- namespace that the client sent was dropped. Its detail names the header. See headers.
INGRESS_CLAIMED_HEADER_DROPPEDA header that a [[cache.policy.vary]] entry claims was dropped from the client’s request, so a caller cannot choose its own value. Its detail names the header, never its value.
CLIENT_MIN_VERSION_UNREADABLEA client profile sets a min_version that is not a valid version, so the floor is ignored and no request is refused for it. Your traffic is unaffected; the setting is not doing anything.
CLIENT_PERSISTED_QUERY_UNENFORCEABLEA client profile requires persisted queries while persisted_query.enabled is false (for example with preset = "off", the default), so the service offers the client nothing it could send. The requirement is ignored rather than refusing all of that client’s traffic.
CACHE_CLIENT_MAX_AGE_MISSA cached entry was older than the max_age on the matching client profile, so that one request went to the origin. The response reports PASS with CACHE_SKIPPED_ENTRY_NOT_REPLACED. The entry is unchanged and still serves other clients.

See persisted queries for the switches these belong to.

CodeStatusRefinesMeaning
PERSISTED_QUERY_NOT_SUPPORTED200The request carried a hash, and this service does not resolve persisted queries (persisted_query.enabled is false). Sent to the client as a GraphQL error, with HTTP 200 so that Apollo clients read it rather than treating it as a transport failure.
PERSISTED_QUERY_NOT_FOUND200A persisted query hash is not registered at this location. Under APQ this is the normal signal for the client to resend the document. Sent as a GraphQL error with HTTP 200, for the same reason.
PERSISTED_QUERY_REGISTEREDA client registered a persisted query with this service.
PERSISTED_QUERY_HASH_MISMATCHA persisted query was offered under a hash that is not its own and was not registered. The operation still runs, so the client receives your origin’s status.
PERSISTED_QUERY_MUTATION_OVER_GET405METHOD_NOT_ALLOWEDLog only. The caller receives METHOD_NOT_ALLOWED. An operation sent as a GET, as a document or as a persisted query hash, is not a query.
PERSISTED_QUERY_REFUSED403FORBIDDENLog only. The caller receives FORBIDDEN. An operation that is not in this service’s registry was refused.
PERSISTED_QUERY_REGISTRY_UNAVAILABLE403FORBIDDENLog only. The caller receives FORBIDDEN. The registry could not be read, so the operation was refused.
PERSISTED_QUERY_UNTRUSTED_OPERATIONAn operation ran that only_registered would have refused. Emitted while audit_unregistered is on, so you can see what your manifest is still missing.

A few fields of request telemetry carry stable values of their own rather than codes. They are what the Lookup and policy details in the Insights request detail are drawn from.

What the cache lookup found. Empty when no lookup ran (a pass decided before the lookup, or lean mode).

ValueMeaning
FOUNDAn entry was found and answered the request, as a HIT or a STALE.
NOT_FOUNDThe cache held no usable entry for the key.
UNREADABLE_NO_LENGTH, UNREADABLE_BODY, UNREADABLE_METADATAAn entry was found but could not be read. It is replaced with your origin’s answer; there is nothing to do.
DECLINED_CLIENT_MAX_AGEAn entry was found and was older than the max_age of the client profile that asked. The entry is left as it is.
FAILEDThe lookup itself failed: the cache was not reached (CACHE_PASS_READ_DISABLED).

Which policy decided whether the response was stored, recorded whether the decision was to store or to refuse.

ValueMeaning
ORIGIN_HEADERYour origin’s Cache-Control decided, outranking any schema policy.
SCHEMA_POLICYThe schema’s @cacheControl policy decided.
NONENeither applied: no origin header that decides, and no schema hint for what the query selected.

With shielding switched on, three further fields record the hop to the shield. shield_pop is the shield location’s alias, shield_node the individual node within it that answered, and shield_result how the hop ended:

ValueMeaning
HITThe shield answered from its own cache.
MISSThe shield had no usable entry, fetched the response from your origin and stored it.
STALEThe shield answered a stale entry, per its own stale handling.
PASSThe shield stored nothing for this request: it did not use its cache, did not store your origin’s response, or answered without its cache, for example because your origin failed or the request was refused there. The shield’s own reason is recorded on its entry.
FALLBACKThe request could not be sent through the shield, so this edge location went to the origin itself. See SHIELD_UNAVAILABLE_FALLBACK.
TIMEOUTNo usable response came back from the shield: the hop exceeded its time budget, or the connection dropped or the response was incomplete, oversized or malformed. The caller receives 504. See timeouts on a shielded service.
UNAVAILABLEA mutation could not be sent through the shield, so this edge location answered 502 rather than send it to your origin itself. See SHIELD_UNAVAILABLE_NO_FALLBACK.

shield_node is empty when no shield answered: on FALLBACK, UNAVAILABLE and TIMEOUT.

The portal’s Insights view shows them in the request detail panel, and can filter the list by the shield result.

The GraphPilot API reports errors in the GraphQL response format, in an ordinary 200 response. The exception is a credential that is presented and refused: an sk_ key that is not a usable key, and any other bearer value that does not verify as a sign-in token, are refused with 401 and UNAUTHENTICATED before any operation runs, an sk_ key that could not be checked at that moment with 503 and SERVICE_UNAVAILABLE, and an unrecognized sk_ key sent over the validation budget with 429 and TOO_MANY_REQUESTS. See when a credential is refused. POST /purge, the REST form of two purge mutations, carries the same codes and answers each with a matching HTTP status instead of 200.

Most errors carry their code in extensions.code; some refusals carry it in extensions.error_code instead. Read both, and branch on the code.

{
"errors": [
{
"message": "Not authenticated to an organization",
"extensions": { "code": "UNAUTHENTICATED" }
}
],
"data": null
}
CodeMeaning
UNAUTHENTICATEDThe request carried no valid credential, or the credential is not scoped to an organization. In a 200 for a missing credential; in a 401 for an sk_ key that is not a usable key and for any other bearer value that does not verify.
SERVICE_UNAVAILABLEAn sk_ key could not be checked at that moment. In a 503 with Retry-After; it says nothing about the key, so send the request again.
FORBIDDENThe credential is valid but lacks the permission the operation needs. Some operations name the missing subject and action in extensions. Also returned when an API key asks for or spends a confirmation code, and when the service did not accept the purge request’s signature, which a redeploy of the service resolves.
NOT_FOUNDThe thing addressed does not exist in the caller’s organization. A service in another organization is reported the same way, as Service not found.
BAD_REQUESTThe input was refused, for example a surrogate key that is too long, too many keys in one purge, or a purgeByQueryHash with a variablesHash but no operationName for which your request telemetry holds no operation name (pass operationName, or null for an anonymous operation). The message says which.
BAD_USER_INPUTAn argument over a documented bound, such as a persisted query push over 100 documents or a document over 100,000 bytes. The message names the bound, so the caller can chunk.
PRECONDITION_FAILEDThe operation cannot run in the current state, for example a purge against a service that has never been deployed or has no active hostname, or a deployment that asks for the newest schema or configuration before one was uploaded (send withoutSchema or withoutConfig to deploy without it).
OPERATION_LIMIT_EXCEEDEDOver HTTP in a 400: the operation’s document is over one of the API’s operation limits, checked before it is validated. extensions.limit names the limit (bytes, tokens, depth, selections or aliases) and extensions.max its value.
INVALID_STATUScancelDeployment on a deployment that is no longer STARTED or PENDING.
CONFIG_VERSION_NOT_ALLOWEDcreateDeployment with both withoutConfig and configVersion. Send one of them.
SERVICE_SETUP_NOT_FOUNDapplyServiceSetupSuggestion or keepServiceAsPlainHttp for a service that has no setup.
SERVICE_SETUP_STATE_CONFLICTA setup mutation that the setup’s current state does not allow, for example applying a suggestion while the first deployment is still running. Read Service.setup and its canRetryDetection, canApplySuggestion and canKeepAsPlainHttp.
SERVICE_SETUP_SUGGESTION_INCOMPLETEapplyServiceSetupSuggestion for a suggestion without a schema, because introspection did not answer. Read the schema with gpilot schema pull and deploy it.
SETUP_WOULD_OVERWRITE_CONFIG, SETUP_WOULD_OVERWRITE_SCHEMAapplyServiceSetupSuggestion for a service whose newest configuration or schema upload is your own, not one the setup wrote. The setup never replaces your files: set request.graphql_paths in your own gpilot.toml and run gpilot deploy. Retrying the detection and keeping plain HTTP stay allowed.
ASSET_CHANGED_CONCURRENTLYcreateDeployment for a service whose schema source is Introspect at deploy, or applyServiceSetupSuggestion: an upload of the service’s configuration or schema landed while GraphPilot was writing the same file, and nothing was written. Send the same request again. After applyServiceSetupSuggestion the setup stays in SUGGESTION_READY.
SERVICE_NOT_LIVEretryServiceSetupDetection before any deployment of the service completed: detection runs through the service’s own hostname, which needs a live deployment.
ORIGIN_TIMEOUT_EXCEEDS_PLANcreateDeployment with a configuration whose origin timeout is above its ceiling on your plan. Nothing is created. The message names the key, the value and the ceiling. See origin timeouts.
RUNTIME_VERSION_YANKEDThe runtime version named in the request (createDeployment with coreVersion, or setServiceRuntimePin) was withdrawn. The message names the version and, where one was given, the reason. Pick another version.
RUNTIME_VERSION_WITHDRAWNcreateDeployment without coreVersion for a service pinned to a runtime version that was withdrawn. A withdrawn version can no longer be deployed, so nothing is created. The message names the pinned version and, where one was given, the reason. Pin a supported version or remove the pin.
INVALID_CONFIRMATION_CODEThe confirmation code was never issued for this action and subject, has expired, has run out of attempts, or is wrong. The four cases read the same. Request a new code.
TOO_MANY_REQUESTSToo many confirmation codes requested: wait a minute and try again. Also, in a 429 with Retry-After, an sk_ key the API has not seen recently, sent while its client is over the validation budget for unrecognized keys. The key was not checked, so it says nothing about the key: send the request again after Retry-After.
ACCOUNT_LOCKEDThe billing account funding the organization is locked, so every mutation outside billing is refused (queries still answer). extensions.reason says why: PAYMENT_FAILED (a payment is overdue: pay the outstanding invoice), FREE_TIER (a Glider (Free) account was locked under fair use this month: the lock lifts at the start of the next month (UTC), or at once on a paid plan once no invoice is outstanding) or MANUAL (contact support). extensions.fields lists the refused root fields. POST /purge answers it with 403.
ORGANIZATION_SLUG_TAKENAnother organization already uses that slug.
ORGANIZATION_QUOTA_EXCEEDEDcreateOrganization when your billing account already funds as many organizations as it may, 3 on every plan. Nothing is created. Delete an organization, or contact support to fund more. See organizations per billing account.
TERMS_NOT_ACCEPTEDcreateOrganization, or subscribePlan for a paid plan, without an acceptance of the terms in force. extensions.reason is missing (no acceptance sent) or outdated (another version than legalTerms.version), and extensions.currentVersion names the version to accept. Nothing is created or ordered. See terms acceptance.
BUSINESS_CONFIRMATION_REQUIREDsubscribePlan for a paid plan without the confirmation of ordering as a business (extensions.reason not_confirmed). Nothing is ordered.
ACCOUNT_LOCKEDA mutation outside billing while the billing account that funds the organization is locked. extensions.reason is PAYMENT_FAILED or MANUAL, and extensions.fields names the refused root fields. The whole operation is refused; queries are never refused. Over HTTP and WebSocket alike.
ACCOUNT_DELETION_IN_PROGRESSAny mutation, billing included, in an organization that is being deleted, except a support request. extensions.scope is ACCOUNT (the account is being deleted) or ORGANIZATION (the organization is), and extensions.fields names the refused root fields.
OUTSTANDING_INVOICEA plan order while an invoice is still unpaid. Pay the outstanding invoice first.
PAYMENT_IN_PROGRESSA plan order while an earlier order is still being completed. Confirm its payment from the invoice if the bank asks, otherwise wait a minute and try again.
PLAN_CHANGE_PENDINGA plan order while an earlier plan change is waiting for the bank to confirm its payment. Confirm it from the invoice, or try again once it is discarded after about 23 hours. See when your bank asks you to confirm the payment.
PAYMENT_FAILEDThe payment method was declined.
RATE_LIMITEDThe payment provider is rate limiting. Try again later.
UNAVAILABLEThe payment provider could not be reached.
INVALID_RETURN_URLA billing checkout named a return URL the portal does not own.
DOMAIN_ALREADY_ATTACHEDThe hostname is already attached to a service.
DOMAIN_INVALID_FORMATNot a valid hostname.
DOMAIN_QUOTA_EXCEEDEDThe plan’s custom domain allowance, pooled across every organization the billing account funds, is used up.
DOMAIN_NOT_IN_PLANCustom domains are not included in the plan.
DOMAIN_NOT_VERIFIEDThe organization has not verified a domain that covers this hostname.
DOMAIN_NOT_RETRYABLEOnly a failed domain can be retried.
SERVICE_DOMAIN_LAST_ACTIVEA service must keep at least one active domain.
SERVICE_DOMAIN_IS_PRIMARYA service must have exactly one primary domain, so the primary cannot be removed.
SERVICE_DOMAIN_PRIMARY_NOT_ACTIVEThe primary domain must be active.
SERVICE_DOMAIN_PLATFORM_PERMANENTThe platform domain can be disabled but not removed.
INTERNAL_ERROR, INTERNAL_SERVER_ERRORA failure on the GraphPilot side. The message stays generic; retrying is the available action.

Some refusals carry extensions.error_code, and where it applies field (the input field) and description.

CodeMeaning
INSUFFICIENT_PERMISSIONSA read or write of organization data without a valid credential or permission. Message Insufficient permissions.
SERVICE_QUOTA_EXCEEDEDThe plan’s service limit is reached. See quotas.
SUBDOMAIN_ALREADY_EXISTSAnother service of the organization already has this subdomain. For a service directly under graphpilot.cloud (created before hostname labels), any other such service.
SUBDOMAIN_INVALID_LENGTHA subdomain must be between 2 and 63 characters.
SUBDOMAIN_INVALID_FORMATA subdomain may contain only lowercase letters, numbers and hyphens.
SUBDOMAIN_RESERVEDThe subdomain is reserved. Only for a service directly under graphpilot.cloud; under a hostname label every valid name is allowed.
ORIGIN_URL_INVALIDThe origin URL must be a valid HTTP or HTTPS URL.
COLOR_INVALID_FORMATA service color must be a hex color code.
ORIGIN_URL_PATH_NOT_ALLOWEDThe origin URL may name only the scheme, host and port, such as https://api.example.com. Remove the path, query or fragment; the paths served as GraphQL are set with request.graphql_paths.
SERVICE_CAPACITY_EXHAUSTEDNew services cannot be created right now, through no fault of the request. GraphPilot is notified automatically. Try again later.
DOMAIN_ALREADY_ATTACHED, DOMAIN_INVALID_FORMATAs in extensions.code, for a write that was refused.
UNIQUE_VIOLATION, CHECK_VIOLATION, FOREIGN_KEY_VIOLATION, NOT_NULL_VIOLATIONA value already exists, is invalid, references a record that does not exist, or is missing. The message says which.

A deployment that fails after it was created reports why in Deployment.errorMessage, in the portal and in gpilot. A failure to load an auth provider’s key set starts with one of these codes, names the provider and every source that was tried, and stops the deployment before it is activated, because the new version would otherwise refuse every token for that provider with 401.

A deployment that ends CANCELLED because GraphPilot stopped it carries a reason in Deployment.errorMessage too, without a code: Replaced by a newer deployment of this service before it went live. when a newer deployment of the same service replaced it, and Cancelled before it went live. otherwise. A cancellation that was requested records none.

CodeMeaning
JWKS_URL_REFUSEDThe provider’s jwks_url is not a URL GraphPilot fetches: it does not parse, its scheme is not https, it has no host, or it carries credentials. Plain http is accepted only on localhost, 127.0.0.1 and [::1]; on any other host it is refused with a key set must be served over https. Fix the URL and deploy again.
JWKS_FETCH_FAILEDNo source answered with a usable response (a failed connection, a non-2xx status, or a body that is too large). The step is retried, since the issuer may be back a moment later.
JWKS_NO_USABLE_KEYA source answered, but what it served holds no key the provider can verify with: not JSON, no keys, an empty set, or only keys of another type than the provider’s algorithms need.

The gpilot CLI’s exit codes are stable, so scripts can branch on them.

CodeMeaning
0Success, or you canceled at a confirmation prompt, or you stopped watching a deployment (Ctrl+C, then “stop watching”).
1General error: invalid input, something not found, a command that refused to run without --yes, or a deployment that failed, was canceled from somewhere else, or whose wait timed out.
2Usage error: an unknown command, flag or value.
3Authentication failed: no usable credential, or the GraphPilot API rejected it (HTTP 401). A permission the credential lacks is 4, not 3.
4Network or GraphPilot API error, including a GraphQL error in the API’s answer.
5Not used. A new deployment cancels one in progress instead of conflicting with it, so no command returns it, and it will not be given a new meaning.
6This CLI version is too old. Upgrade it (see staying up to date).
7The deployment’s outcome is unknown: watching it ended before it reported one (the progress stream closed, or could not be opened), and checking the deployment until --timeout ran out did not settle it. It is still open, or every check failed. Check with gpilot status.
130, 143gpilot service create --deploy was stopped while following the setup: Ctrl+C gives 130, SIGTERM gives 143 (Unix). It prints its normal output first (with -o, the one document, signing key included), and the setup goes on at GraphPilot; follow it with gpilot service setup --service <subdomain>.