Errors & audit reasons¶
Every refusal TDB makes, in one place: what the caller sees, what the audit log records,
and what to do about it. Search this page for the code or the reason you are
looking at.
Each refusal writes a signed denied entry to the audit log with an action (what
was attempted) and a reason (why it was refused). See Audit log
for the entry format.
REST status codes¶
| Status | Audit reason |
What it means | What to do |
|---|---|---|---|
| 400 | sql_validation_failed |
The SQL is not a single read-only SELECT (or WITH … SELECT) |
Remove the write or the second statement. See Query |
| 400 | — | Query failed: … — the source rejected the SQL: unknown column or table, syntax error, a value that does not convert (from community 0.12.4 / enterprise 0.20.3) |
Fix the SQL; the source's own message follows |
| 400 | limit_exceeds_max |
limit is above the deployment's row ceiling (TDB_MAX_ROWS, enterprise) |
Lower limit, aggregate in SQL, or raise TDB_MAX_ROWS |
| 400 | invalid_connection |
Registration: the connection block is not valid for the source_type |
Check the connector page for the required fields |
| 400 | unsupported_source_type |
Registration: the source_type is not available in this edition |
Use csv (community) or a commercial connector |
| 401 | missing_credentials |
No Authorization header |
Send Authorization: Bearer <key> |
| 401 | invalid_api_key |
The key is not recognised, or has been revoked | Check the key; rotate if lost |
| 401 | invalid_or_expired_token |
A JWT failed its signature or has expired | Issue a fresh token |
| 403 | insufficient_role_required_<role>_have_<role> |
The key's role is below what the endpoint needs (enterprise) | Use a key with the role named in the reason. See RBAC |
| 403 | tool_not_permitted_<tool> |
The key's allowed_tools does not include the tool this endpoint uses (enterprise) |
Add the tool to the key, or use another key |
| 403 | sql_file_access |
The SQL named a file outside the source's data directory (CSV, Parquet), or any other file (DuckDB) | Query the registered table (data) only |
| 403 | path_outside_allowed_dir |
The registered file resolves outside TDB_ALLOWED_DATA_DIR |
Move the file under the data directory |
| 403 | privileged_db_role |
The source connects as a database role that can reach the server's files or programs (enterprise, from 0.12.0) | Register it with a least-privileged role. See PostgreSQL |
| 403 | invalid_origin |
A request from a browser page on another origin | Add the origin to TDB_CORS_ORIGINS (enterprise) |
| 403 | — | license_expired body: the licence is expired or invalid (enterprise) |
Contact us for a renewed licence |
| 404 | source_not_found |
No source with that name or UUID | GET /v1/sources lists them |
| 409 | registry_conflict |
A source with that name already exists | Pick another name, or delete the old source |
| 409 | — | An audit export to the same destination is already running (enterprise) | Wait for it, or treat 409 as "still running" |
| 422 | — | The request body failed validation (wrong type, value out of range; in community a limit over 1,000) |
Read the detail array; it names the field |
| 429 | rate_limit_exceeded |
The key's per-minute budget is used up (DB-managed keys) | Retry after the X-RateLimit-Reset time |
| 500 | — | TDB or the source failed: unreachable source, out of memory. Not the caller's SQL | Check the source and the application log |
| 503 | file_unreadable |
The file behind a CSV, Parquet or DuckDB source is missing or unreadable | Restore the file or fix its permissions |
| 503 | source_busy |
Another process has the DuckDB file open for writing (enterprise, from 0.23.0) | Retry once the writer has closed the file. See DuckDB |
| 503 | — | TDB_JWT_SECRET is not set and a JWT was presented (enterprise) |
Set it, or use API keys |
| 504 | query_timeout |
The query ran longer than TDB_QUERY_TIMEOUT and was cancelled at the source (enterprise database sources) |
Narrow the query, add an index, or raise the timeout |
MCP¶
An MCP call can fail in three places.
At the transport (HTTP status, before the tool runs): 401 for a missing or invalid
key (audited missing_api_key or invalid_api_key), 403 for a foreign Origin
(invalid_origin), 429 for the rate limit (rate_limit_exceeded), and 400 or 404 for
protocol problems. The JSON-RPC codes are listed in
MCP: error codes.
As a tool error (HTTP 200, isError: true, a readable message in content): the
same refusals as REST — sql_validation_failed, sql_file_access,
source_not_found, limit_exceeds_max, privileged_db_role, query_timeout, source_busy — plus:
Audit reason |
What it means | What to do |
|---|---|---|
prompt_injection_detected |
The input filter found instruction-like text in the SQL (enterprise) | Remove the embedded instructions |
tool_not_permitted_<tool> |
The key's allowed_tools does not include this tool (enterprise) |
Add the tool to the key |
| — | A Lean Mode value is invalid, e.g. max_cell_chars: 10 |
Use a value the message lists |
As a redaction (the call succeeds): a cell the output filter flags is replaced in the result, and the redaction is counted in the server log (enterprise).
PostgreSQL wire gateway¶
The gateway answers with standard PostgreSQL error codes (SQLSTATE), which every
client shows. Enterprise only.
| SQLSTATE | Audit reason |
What it means | What to do |
|---|---|---|---|
28000 |
wire_requires_read_role |
The key or token is not read-role |
Use a read-role key as the password |
28000 |
— | The client did not use TLS, or the licence is not valid | Connect with sslmode=require or verify-full |
28P01 |
missing_credentials, invalid_api_key, invalid_or_expired_token |
No password, or it is not a valid key or token | Check the key |
3D000 |
— | No TDB source matches the database name | Use a registered PostgreSQL source's name as the database |
42501 |
sql_validation_failed, privileged_db_role |
A statement was refused, or the source's role is privileged | As for REST |
25001 |
statement_after_transaction_end |
A statement followed COMMIT/ROLLBACK in the same batch |
Send it in a new batch |
53300 |
— | TDB_PG_MAX_CLIENTS connections are already open |
Close idle clients or raise the limit |
53400 |
rate_limit_exceeded |
The key's rate limit | Slow down, or raise the key's limit |
54000 |
row_cap_exceeded |
A result went over TDB_PG_MAX_ROWS; the connection is closed |
Aggregate, or add LIMIT |
57014 |
query_timeout |
Cancelled by TDB_QUERY_TIMEOUT or by the client |
Narrow the query or raise the timeout |
58000 |
— | TDB could not write the audit entry, so the statement was refused (fails closed) | Check the audit log volume: space and permissions |
More on the gateway: PostgreSQL wire gateway.
Column and row policies¶
Enterprise Mid-Market, from 0.22.0, while TDB_POLICIES_FILE is set. A refusal is
403 Refused by policy: … on REST, a tool error on MCP and 42501 on the
wire gateway, audited with the reason below. Under a rule the entry also carries
policy_rule. See Column and row policies.
Audit reason |
What it means | What to do |
|---|---|---|
policy_no_rule |
No rule names this key or its role, and the source's default is deny |
Add a rule for the key, or use a key that has one |
policy_missing_attribute |
The key's rule has a row filter needing an attribute the caller lacks | Set it: PUT /v1/auth/keys/{id}/attributes, or the JWT attributes claim |
policy_table_not_permitted |
The SQL reads a table, view or copy the caller's rule does not name | Query only the rule's tables, or add the table to the rule |
policy_function_not_allowed |
A function outside the allow-list for policied callers (functions that run SQL text or read files, among others) | Rewrite without it; ask us if an everyday function is missing |
policy_unclassified_table |
A table expression TDB cannot place (a table function, a name passed as a string) | Name the table directly |
policy_unparseable |
TDB could not parse the SQL, or it holds more than one statement | Simplify the SQL; send one statement |
policy_not_a_query |
Not a SELECT, or a SELECT INTO / locking clause |
Send a plain SELECT |
policy_no_visible_columns |
The rule leaves no column of a table visible | Fix the rule |
policy_schema_unavailable |
TDB could not read the table's columns, so it cannot build the rewrite (fails closed) | Check the source is reachable |
policy_unknown_caller |
TDB could not tell who is asking | Authenticate with an API key or JWT |
policy_unsupported_source |
The source type has no policy support | Remove the source from the policy file |
policy_catalog_refused |
Gateway: a catalog that holds data (pg_stats and similar) |
Not available to policied keys |
policy_registry_change |
A non-admin key a policy restricts tried to register or delete a source | Use an admin key |
A SQL error under a rule (400 Query failed: …) shows only the reason, such as
column "diagnosis" does not exist. A withheld column reads exactly like one that is
not there.
At startup, TDB refuses to start, naming the problem, when the policy file is
missing, not valid YAML or has an invalid field, or when the licence's tier is smb
(policies_tier_refused in the application log).
Single sign-on and SCIM¶
Reasons in the audit log from single sign-on. A person sees a plain page with no detail; the entry has the rest.
Audit reason |
What it means | What to do |
|---|---|---|
sso_no_mapped_group |
No group this person is in is in TDB_SSO_FILE |
Add them to a mapped group; check the IdP sends the groups claim |
sso_groups_overflow |
The IdP sent a pointer in place of a long group list | Limit the groups the IdP sends to TDB |
sso_user_inactive |
The person is deactivated or has no access | Reactivate them in the IdP |
sso_user_not_provisioned |
TDB_SSO_JIT=false, and SCIM has not provisioned them |
Assign them in the IdP |
sso_email_unverified |
A provisioned account exists for this address, and nothing vouches for it | Have the IdP send email_verified; see TDB_SSO_TRUST_EMAIL |
sso_id_token_invalid |
The IdP's ID token failed a check: signature, issuer, audience, expiry or nonce | Check TDB_OIDC_ISSUER and TDB_OIDC_CLIENT_ID; the entry's detail says which |
sso_idp_unavailable |
The IdP could not be reached, or answered with an error | Check the IdP and the network; nobody is signed out by this |
sso_idp_refused, sso_idp_error |
The IdP refused the sign-in | Check the client secret and the IdP's own log |
sso_state_mismatch, sso_state_unknown |
The sign-in expired, or finished in another browser | Start again; repeated, it can be an attempted login forgery |
sso_consent_csrf, sso_consent_expired |
The consent page expired, or was posted from another browser | Start again; repeated, it can be an attempted forgery |
sso_consent_denied |
The person chose Deny | None |
sso_refresh_replay |
A used refresh token came back. All of that sign-in's tokens are revoked | Sign in again; if unexplained, the token may have been stolen |
sso_refresh_refused, sso_refresh_unknown, sso_refresh_wrong_user |
The IdP refused the refresh, or the token is not valid | Sign in again |
scim_invalid_token |
A request to /scim/v2 had the wrong token |
Check TDB_SCIM_TOKEN at the IdP |
tool_not_permitted_<tool> |
The person's groups do not allow this tool | Change allowed_tools in TDB_SSO_FILE |
Not refusals¶
truncated: true— not an error. More rows existed than the row ceiling or yourlimitallowed. Aggregate, filter, or ask for fewer columns.trimmed_cellsin a Lean result — long text was cut because you asked formax_cell_chars; each cut is marked…[+N chars].