Skip to main content

GraphQL Error Catalog

The Hub API surfaces errors as standard GraphQL errors. Every error is constructed as a plain async_graphql::Error::new(message) — the API sets no extensions on any error, so there is no machine-readable error code to branch on. This page lists the actual message strings the API emits so apps know what to expect.

Error envelope​

{
"errors": [
{
"message": "Unauthorized: Authentication required",
"locations": [{ "line": 2, "column": 3 }],
"path": ["createUnsignedRideRequest"]
}
],
"data": null
}
No stable error codes

There is no extensions.code field. The only way to distinguish failures programmatically today is to match on message text, which is fragile — these strings are internal format! output and can change in any release without notice. Treat message matching as a stopgap: use it to decide between "retry" and "give up", and always keep a default/rethrow branch. Do not build user-facing logic that depends on an exact string.

Authentication errors​

MessageTriggerApp action
Unauthorized: Authentication requiredMissing, malformed, or expired JWT on a guarded fieldRe-authenticate, then retry
Proof of key ownership failed: challenge timestamp is outside the allowed ±120s windowgenerateToken timestamp too far from server timeSync the client clock and re-sign
Proof of key ownership failed: signature does not match the provided public keyChallenge signed with the wrong private keyRe-sign with the key matching publicKey
Proof of key ownership failed: invalid challenge signature: …r/s/v unparseable or not recoverableCheck the signature encoding
Proof of key ownership failed: Invalid public key length. Expected 40 or 130 characters, got …publicKey is neither a 20-byte address nor a 130-char uncompressed keyValidate key format before calling
Proof of key ownership failed: Invalid hex format for public key: …publicKey contains non-hex charactersValidate key format before calling
Failed to generate token: …JWT signing failed server-sideRetry; check API logs

Guarded fields (those that can return Unauthorized: Authentication required) are all createUnsigned* mutations (including createUnsignedBurn), sendRawTransaction, and the accountBalance query. chainInfo is public, alongside every subscription and every list* query — see Authentication for the full table.

An invalid token is not rejected at the transport level: the handler simply does not attach an authenticated user, so the request only fails if it reaches a guard.

Node client errors​

The Hub API forwards results from the node WebSocket JSON-RPC. The underlying client produces these messages:

MessageTrigger
WebSocket connection not establishedNo live socket to the node (still connecting/reconnecting)
Request timed outNode did not answer within the client's 10s timeout
Connection lost before receiving responseSocket dropped while the request was in flight
Failed to send request: …Write to the node socket failed
Failed to receive response / Mismatched response ID / No result or error in responseMalformed or mismatched node reply
(the node's own JSON-RPC error text, verbatim)Node returned a JSON-RPC error — e.g. bad signature, stale nonce, insufficient balance

Resolvers wrap that text in a prefix identifying the operation, so the message you receive is nested:

  • Failed to get nonce: Failed to get nonce for address 0x…: Request timed out — any createUnsigned* mutation
  • Failed to send transaction: … — sendRawTransaction
  • Failed to get balance: Failed to get balance for address 0x…: … — accountBalance
  • The list* queries and subscriptions pass the node message through unwrapped.

When a transaction is rejected by the node, the nonce may be stale — re-fetch it with a fresh createUnsigned* call before retrying.

Input validation errors​

The API does almost no hand-written input validation — argument types are enforced by the GraphQL schema itself, and malformed queries fail async-graphql's own parsing/validation before any resolver runs. The only explicit check in a resolver is:

MessageTrigger
fare must be positivecreateUnsignedRidePay called with fare <= 0

Coordinates, map bounds, and transaction hashes are not validated by the Hub API. Bad values are forwarded to the node, which rejects them — so those failures arrive as node client errors.

Handling pattern (SDK / app)​

The SDK throws on GraphQL errors (its internal executeGraphQL joins all errors[].message values with newlines into a single Error), so use try/catch rather than inspecting a result object:

try {
const unsigned = await sdk.createUnsignedRideRequest({ pickup, dropoff, fare });
const { rawTransaction } = await sdk.signTransaction(unsigned, privateKey);
return await sdk.submitTransaction(rawTransaction);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);

// Fragile: message matching is the only option — there are no error codes.
if (message.includes('Unauthorized: Authentication required')) {
sdk.setPrivateKey(privateKey); // SDK re-runs generateToken on the next call
return retry();
}
if (
message.includes('Request timed out') ||
message.includes('WebSocket connection not established') ||
message.includes('Connection lost')
) {
await wait(500);
return retry();
}
if (message.includes('Failed to get nonce') || message.includes('Failed to send transaction')) {
// nonce may be stale — rebuild from a fresh createUnsigned* call and re-sign
return rebuildAndResubmit();
}
throw err;
}

Authentication is handled inside the SDK — ensureAuth() is private and runs automatically before every authenticated call. There is no public method to force a token refresh; supplying the private key (constructor or setPrivateKey) is what lets the SDK issue one.

Notes​

  • Error messages are not stable across releases, and there are no error codes — see the warning above
  • GraphQL returns HTTP 200 even for resolver errors — the failure is in the errors[] array, never in the status code
  • Subscriptions emit errors on the same WebSocket stream — handle them in your subscription client. Because the polling loops re-query the node each tick, a transient node failure surfaces as one error payload and the stream keeps running.