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.
Browser sign-in failures
Section titled “Browser sign-in failures”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) |
invalid_token reasons
Section titled “invalid_token reasons”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).”).
session_ended reasons
Section titled “session_ended reasons”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.
$ 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:
$ 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.
$ 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}