Skip to content

Error codes

Every error is an RFC 9457 problem document:

{
"type": "urn:warp-sql:error:permission_denied",
"title": "Permission denied",
"status": 403,
"code": "permission_denied",
"detail": "Hardcharger blocked this statement: user joe has no SELECT on column(s) birthday of my.main.customers. …",
"sqlstate": "42501",
"request_id": "r_5d2…"
}

code is stable: program against it. title is a short summary of the code, the same for every error with that code, and detail says what happened this time; both are for people, and detail may change. sqlstate is the SQL standard’s code when there is one. request_id is also in the server’s logs (and the Warp-Request-Id header), so quote it when you report a problem. The drivers carry all of it: Python’s exception message, JDBC’s SQLException (getSQLState(), WarpProblem.codeOf(e)), Go’s warpsql.ErrorCode(err). A code a client doesn’t know is handled by its HTTP status: 4xx means the request or its statement ended because of what it asked for, 5xx that the server failed or can’t serve now. A statement’s own failure is never 5xx.

Code Status Means What to do
bad_request 400 A malformed request Fix the client
unknown_option, invalid_option_value 400 A session option the server doesn’t have, or a bad value Check the option’s name and value
sql_error 400 DuckDB rejected the SQL: syntax, binding, a missing object (SQLSTATE 42P01), an existing one (42P07), … Fix the SQL; detail says what
invalid_return_to 400 A browser sign-in’s return_to that isn’t a path on the server itself (an absolute URL, //host, a backslash, a fragment, …) Send a path such as /ui/
invalid_credentials 401 Sign-in failed. It never says which part was wrong. Check the user, password or token; an identity-provider token may be expired, for the wrong audience, or for a user not authorized
invalid_token 401 A missing, malformed, expired or foreign access token (another replica’s, without allow_cross_instance_tokens); also a token whose user’s password changed, or who was disabled or dropped, or a revoked scoped token. A token the server issued itself carries a reason (below) Sign in again; after credentials_changed or principal_dropped, the old password no longer works either
session_ended 401 The server ended the session: an administrator killed it, or access control ended it; reason says which (below) Open a new session; except after killed, sign in again first
unsupported_grant_type 400 A sign-in grant the server doesn’t offer, such as client_certificate without certificate sign-in Sign in another way
certificate_required 401 Certificate sign-in on a connection without a client certificate (where certificates are optional) Connect with the certificate
unknown_certificate_identity 401 The certificate has no identity of the kind the server maps, several, or one that is no administrator identity and no enabled user Check --mtls-identity-source; create the user
certificate_revoked 401 The connection’s certificate was revoked (or its CRL expired) after the connection was set up Use a valid certificate
certificate_mismatch 401 A token bound to a client certificate, on a connection without that certificate; or a session opened with another binding Present the certificate the token was issued for
certificate_identity_mismatch 401 With --mtls-require-identity-match: a password or token sign-in for another identity than the connection’s certificate Sign in as the certificate’s identity
permission_denied 403 The user lacks a privilege; also the guards (files, secrets, admin-only commands) Ask for the grant; detail names it
read_only 403 The server or session is read-only Write elsewhere
session_not_found, statement_not_found, prepared_not_found, ingest_not_found 404 No such handle, or not yours: evicted after session_idle_timeout, closed, server restarted; statement_not_found also for a statement the server has forgotten Open a new session (drivers do, and tell you)
partition_not_found 404 The result has no partition with that index Read the partitions the statement document lists
not_found 404 No such endpoint Check the URL
flow_not_found 404 No such browser sign-in, a wrong poll secret, or one already collected or expired (sign-ins last 10 minutes) Start the sign-in again
method_not_allowed 405 The endpoint exists, but not for this method
result_not_ready 409 The statement has no result schema yet Retry
result_not_resumable 409 Re-reading a result that wasn’t retained Submit with Warp-Retain-Result: true
result_expired 410 The retained result expired Run the statement again
ingest_incomplete, ingest_part_exists 409 A partitioned upload committed before every part arrived, or a part sent twice Send the missing parts; send each part once
ingest_expired 410 A partitioned upload waited too long between parts and was dropped Start the upload again
payload_too_large 413 A JSON body over the server’s limit Use Arrow parameters or ingest
unsupported_arrow_type 415 An Arrow type DuckDB can’t store without losing values Cast the column before uploading
session_elsewhere 421 The session lives on another replica Route by session (load balancers)
idempotency_key_reused 422 The same Idempotency-Key with a different request Use a new key for a new request
headers_too_large 431 Request headers over max_header_bytes Raise the limit (large identity tokens)
too_many_sessions 429 Past max_sessions Close sessions, or retry later
rate_limited 429 The statement queue is full, a statement waited longer than max_queue_wait, or more browser sign-ins are in progress than oauth_max_flows Retry after Retry-After
result_quota_exceeded 429 Too many retained results Read and delete results
statement_cancelled 409 The statement was cancelled; cancel_reason says why (client, killed, privilege_revoked, server_shutdown, …)
statement_timeout 409 The statement ran past its timeout and was cancelled (cancel_reason: timeout) Raise the timeout, or speed the query up
execution_error 422 The statement failed while running: a division by zero, a constraint, a conversion detail says why
internal_error 500 The server failed in a way that isn’t the request’s doing (a bug) Report it with the request_id
not_implemented 501 The server doesn’t implement this operation, such as browser sign-in without oauth_client_id
server_draining 503 The server is shutting down Retry after Retry-After, elsewhere

A statement that has failed, or was cancelled, by the time the server answers its submission gets one of these as an error response, with the statement’s id in the Warp-Statement-Id header (and cancel_reason in the problem document of a cancelled one):

{
"type": "urn:warp-sql:error:statement_cancelled",
"title": "Statement cancelled",
"status": 409,
"code": "statement_cancelled",
"detail": "the statement was cancelled (client)",
"request_id": "r_kdel…",
"sqlstate": "57014",
"cancel_reason": "client"
}

A failed or cancelled statement document, from polling a statement or from a replay of an idempotent request (200), carries the same problem document in error: treat it as the statement’s error whatever the status.

A failed streamed result ends without Arrow’s end-of-stream marker: the HTTP status was already 200, so the cut stream is the error. Clients must treat it as failed and fetch the statement document for the reason. The drivers do.

Codes a client makes up for a failure it found itself, with no problem document from the server (a broken connection, a response it can’t read), begin with client_, such as the JavaScript client’s client_transport_error and client_protocol_error. The server never sends a client_ code.

A browser sign-in that fails is reported once, by the poll that would have received the token, as {"state": "failed", "error": {"code", "detail"}}:

error.code When
access_denied You, or the identity provider, refused the sign-in
invalid_credentials The provider’s answer didn’t verify, or names someone who may not sign in (token_authorized_emails; under access control, no store user)
identity_provider_error The server couldn’t finish with the provider (unreachable, the code was refused: often a redirect URI the provider doesn’t have registered)

When the server refuses a token it issued itself, and the token is unaltered, the problem document’s reason says why it stopped working:

reason When
token_expired The token’s lifetime ended
token_revoked The token was revoked: a scoped token dropped with DROP SCOPED TOKEN
credentials_changed The user’s password changed (ALTER USER … WITH PASSWORD) after the token was issued
principal_dropped The user was dropped or disabled

A token the server can’t verify as its own (malformed, signed with another key, altered) gets invalid_token with no reason, whatever user it names: only the holder of a real token learns anything about that user. The same token never works again, so don’t retry with it. A client that holds the credentials it signed in with may sign in again (once) after token_expired, no reason, or a reason it doesn’t know; after token_revoked, credentials_changed or principal_dropped someone withdrew the access, or the old credentials fail too, so the drivers don’t sign in again on their own and report the reason instead. The hardcharger CLI prints the reason (“The server no longer accepts this sign-in: the user’s password or credentials changed (credentials_changed). Sign in again (.connect).”).

reason When
killed An administrator ran KILL SESSION
principal_dropped The user was dropped or disabled
credentials_changed The user’s password changed, or access control revoked something else the session depended on (such as its tenant’s catalogs)
token_revoked The scoped token that opened the session was revoked
token_expired The scoped token that opened the session expired: the session ends then

A killed session answers session_ended at once, since its token still stands. When access control ends a session, the token that opened it stops working first, so its next request gets invalid_token with the same reason; after the client signs in again, the old session answers session_ended with the reason. After credentials_changed, principal_dropped, token_revoked or token_expired a client needs a new token before it opens a new session; after killed (or a reason it doesn’t know) it may open one with the token it has. The drivers renew a token and open a new session on their own only after token_expired, and report the other reasons. Any reason a client doesn’t know is a generic one: the session is gone.

Terminal window
$ export HARDCHARGER_PASSWORD='change-me'
$ hardcharger --host localhost --tls=false --username hardcharger --quiet --command "CREATE USER joe WITH PASSWORD 'old-secret'" < /dev/null > /dev/null
$ token() { curl -s localhost:31337/warp/v1/auth/token -H 'Content-Type: application/json' \
-d "{\"grant_type\": \"password\", \"username\": \"joe\", \"password\": \"$1\"}" | jq -r .access_token; }
$ TOKEN=$(token old-secret)
$ SESSION=$(curl -s localhost:31337/warp/v1/sessions -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{}' | jq -r .session_id)
$ hardcharger --host localhost --tls=false --username hardcharger --quiet --command "ALTER USER joe WITH PASSWORD 'new-secret'" > /dev/null; sleep 2
$ curl -s localhost:31337/warp/v1/sessions/$SESSION -H "Authorization: Bearer $TOKEN" | jq -c '{code, reason}'
{"code":"invalid_token","reason":"credentials_changed"}
$ curl -s localhost:31337/warp/v1/sessions/$SESSION -H "Authorization: Bearer $(token new-secret)" | jq -c '{code, reason}'
{"code":"session_ended","reason":"credentials_changed"}

A dropped scoped token gets token_revoked, while a token the server can’t verify as its own (here, the same token with its signature replaced) gets no reason at all:

Terminal window
$ export HARDCHARGER_PASSWORD='change-me'
$ SCOPED=$(hardcharger --host localhost --tls=false --username hardcharger --quiet --csv \
--command "CREATE SCOPED TOKEN nightly FOR USER joe ALLOW SELECT VALID FOR '1 hour'" | tail -1 | cut -d, -f6)
$ SESSION=$(curl -s localhost:31337/warp/v1/sessions -H "Authorization: Bearer $SCOPED" \
-H 'Content-Type: application/json' -d '{}' | jq -r .session_id)
$ hardcharger --host localhost --tls=false --username hardcharger --quiet --command "DROP SCOPED TOKEN nightly FOR USER joe" > /dev/null; sleep 2
$ curl -s localhost:31337/warp/v1/sessions/$SESSION -H "Authorization: Bearer $SCOPED" | jq -c '{code, reason}'
{"code":"invalid_token","reason":"token_revoked"}
$ curl -s localhost:31337/warp/v1/sessions/$SESSION -H "Authorization: Bearer ${SCOPED%.*}.forged" | jq -c '{code, reason}'
{"code":"invalid_token","reason":null}

A session opened with a scoped token ends when the token expires. Its token gets token_expired. A new password sign-in has different authority from the scoped token, so it cannot access that token’s session: it gets session_not_found. Sessions belong to both the user and the credential’s authority, preventing credentials with different permissions from sharing a session.

Terminal window
$ SHORT=$(HARDCHARGER_PASSWORD='new-secret' hardcharger --host localhost --tls=false --username joe --quiet --csv \
--command "CREATE SCOPED TOKEN short ALLOW SELECT VALID FOR '3 seconds'" | tail -1 | cut -d, -f6)
$ SESSION=$(curl -s localhost:31337/warp/v1/sessions -H "Authorization: Bearer $SHORT" \
-H 'Content-Type: application/json' -d '{}' | jq -r .session_id)
$ sleep 5
$ curl -s localhost:31337/warp/v1/sessions/$SESSION -H "Authorization: Bearer $SHORT" | jq -c '{code, reason}'
{"code":"invalid_token","reason":"token_expired"}
$ JOE=$(curl -s localhost:31337/warp/v1/auth/token -H 'Content-Type: application/json' \
-d '{"grant_type": "password", "username": "joe", "password": "new-secret"}' | jq -r .access_token)
$ curl -s localhost:31337/warp/v1/sessions/$SESSION -H "Authorization: Bearer $JOE" | jq -c '{code, reason}'
{"code":"session_not_found","reason":null}