fix(errors): stop blaming the token for every HTTP 401 (#711) - #717
Conversation
`BaseHttpClient._raise_api_error` mapped every 401 to `INVALID_TOKEN` with
the message "Invalid or expired token", regardless of what the server
actually said. Keboola Metastore answers a 401 with
`{"exception": "Failed to create project scope"}` -- an internal
project-scope resolution failure -- for a token the Storage API accepts on
the very same stack. That blocked every `semantic-layer` / `sl` command
while `project status` reported the token healthy, and the generic wrapper
sent the reporter checking expiry and master-token status for a fault that
was entirely server-side.
A 401 whose own error text does not describe a bad or expired credential
now raises the new `ErrorCode.AUTH_REJECTED`, quoting the server verbatim
and saying explicitly that rotating the token is unlikely to help. A body
that says nothing at all (empty, or `{}`) keeps `INVALID_TOKEN` -- silence
is the textbook rejected-credential response, and diverting it would be
over-reading. Exit code is 3 for both, so `$?`-based callers see no change.
Also fixes a second defect found in the same method: the 401 / 403 / 404
branches raised *before* the `exceptionId` suffix was appended, so the one
handle Keboola support traces an incident by was dropped on exactly the
auth errors where the fault is server-side. The reporter had to fall back
to raw curl to obtain it. This is the same gap #599 closed for 5xx, missed
on the early-return branches.
The Metastore-side 401 itself is a server issue and is not addressed here.
…the opaque 401 (#711) The Metastore's auth middleware collapses every project-scope resolution failure into a 401 'Failed to create project scope' (go-monorepo services/metastore/internal/middleware/auth.go resolveProjectScope; the underlying MasterTokenRequiredError is logged server-side and discarded). Unlike the Storage API, the metastore accepts only a MASTER (project admin) Storage token, so every valid non-master token lands on that 401 -- exactly what issue #711 hit (is_master_token: false is right in the report). A/B-verified live on us-east4.gcp: non-master token -> this 401 on every semantic-layer call; master token on the same stack -> passes. MetastoreClient now funnels every request through a reclassification: that specific 401 becomes MISSING_MASTER_TOKEN (same exit 3, authentication class, mirroring the token create / config oauth-url pre-flight guards from #599) with the actual remedy in the message and the [exceptionId: ...] suffix carried over. Other unexplained 401s keep the AUTH_REJECTED mapping from the previous commit; 401s that do blame the credential keep INVALID_TOKEN. Also corrects the stale 'semantic- layer is a gated feature' claim in tests/helpers.py and syncs every doc surface (error-codes, gotchas, commands-reference, semantic-layer workflow, keboola-expert, CLAUDE.md, kbagent context).
Verification + root cause found — pushed a follow-up commitI verified the issue live and traced the server-side cause, which turns out to be deterministic and known — so the headline case deserves a sharper diagnosis than Root cause (server-side, by design)
In other words: unlike the Storage API, the Metastore accepts only a MASTER (project admin) Storage token. Every valid non-master token gets this 401 on every call. The reporter's tokens were non-master ( Live A/B verification (us-east4.gcp, same stack, same commands)
Also reproduced on Follow-up commit
The generic Verified live on the branch: non-master token now gets Worth considering upstream (out of scope here): the metastore middleware could surface |
…tchas.md section collision)
Batches the seven PRs merged since v0.91.0 into one version bump, one changelog entry and one set of resolved version gates: - #719 (#714) `flow triggers` -- table triggers, not just cron - #717 (#711) a 401 is no longer automatically blamed on the token - #722 (#704) setup completes in chat; skill covers setup + logout - #718 (#716) `--conversation-id` global flag - #706 223 stale version gates retired at the 0.80.0 floor - #702 release process enforced rather than remembered - #721 `get_flow_detail` docstring fix Includes a curated What's-new entry (#717's error rework is UI-visible on the Semantic Layer page) and the step 8-11 silent-drift review.
What
Two defects in
BaseHttpClient._raise_api_error, both surfaced by #711.1. Every 401 was reported as a token problem. The method mapped any 401 to
ErrorCode.INVALID_TOKENwith the messageInvalid or expired token (token: ...): {api_message},regardless of what the server said. Keboola Metastore answers a 401 with
{"exception": "Failed to create project scope"}— an internal project-scope resolution failure —for a token the Storage API accepts on the very same stack. That blocked every
semantic-layer/slcommand on the reporter's stack whilekbagent project statusreported the same token healthy(
is_master_token: false, expiry months out).A 401 whose own error text does not describe a bad or expired credential now raises a new
ErrorCode.AUTH_REJECTED, quotes the server verbatim, and states explicitly that rotating the tokenis unlikely to help.
2.
exceptionIdwas dropped on 401 / 403 / 404. Those three branchesraisebefore the[exceptionId: ...]suffix is built, so the one handle Keboola support traces an incident by neverreached the operator — which is why the report had to fall back to raw
curlto obtainmetastore-fbfeCiBSXXBLk7D. Same gap #599 closed for the 5xx family, missed on the early-returnbranches.
Why this shape
INVALID_TOKEN. An empty body, or{}, is the textbookrejected-credential response; diverting it to
AUTH_REJECTEDwould be over-reading silence.Only a server that actually said something else earns the new code.
message that mentions a token, credential, expiry, or authentication keeps the historical mapping,
so the new code appears only where the server demonstrably blamed something else.
the diagnosis, not the classification. Callers branching on
$?see zero change; only JSONconsumers reading
error.codeobserve the new value.The Metastore-side 401 itself is a server-side issue on
metastore.europe-west3.gcp.keboola.comand is not addressed here — this PR makes it diagnosable rather than misattributed.
How it was tested
New
TestUnauthorizedErrorMappingintests/test_http_base.py(9 cases), covering the reportedMetastore body,
exceptionIdsurvival on 401/403/404, both regression guards (a genuineInvalid access token, and expiry phrasing that never says "token"), token masking, and theempty-body rule.
A/B verified the tests are not vacuous: with
src/keboola_agent_cli/http_base.pystashed, the 4new-behaviour tests fail and the 4 regression guards still pass.
make checkexits 0 — 6369 passed, lint/format clean,check_error_codes.pyconfirmsdocs/error-codes.mdmatches the enum,check_version_gates.pyclean.Doc surfaces
docs/error-codes.md— newAUTH_REJECTEDrow (CI-enforced against the enum).plugins/kbagent/skills/kbagent/references/gotchas.md— new section tagged(since vNEXT, #711),placed on the body line rather than the heading per the heading rule.
changelog.pyentry — that belongs to the release PR.Fixes #711