Skip to content

fix(errors): stop blaming the token for every HTTP 401 (#711) - #717

Merged
padak merged 3 commits into
mainfrom
claude/issue-711-401-error-mapping
Aug 30, 2026
Merged

fix(errors): stop blaming the token for every HTTP 401 (#711)#717
padak merged 3 commits into
mainfrom
claude/issue-711-401-error-mapping

Conversation

@padak

@padak padak commented Aug 29, 2026

Copy link
Copy Markdown
Member

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_TOKEN with the message Invalid 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 /
sl command on the reporter's stack while kbagent project status reported 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 token
is unlikely to help.

2. exceptionId was dropped on 401 / 403 / 404. Those three branches raise before the
[exceptionId: ...] suffix is built, so the one handle Keboola support traces an incident by never
reached the operator — which is why the report had to fall back to raw curl to obtain
metastore-fbfeCiBSXXBLk7D. Same gap #599 closed for the 5xx family, missed on the early-return
branches.

Why this shape

  • A body that says nothing keeps INVALID_TOKEN. An empty body, or {}, is the textbook
    rejected-credential response; diverting it to AUTH_REJECTED would be over-reading silence.
    Only a server that actually said something else earns the new code.
  • The marker list is permissive in the "it is a token problem" direction — an unrecognised
    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.
  • Exit code stays 3. The response was always an authentication-class failure; what was wrong was
    the diagnosis, not the classification. Callers branching on $? see zero change; only JSON
    consumers reading error.code observe the new value.

The Metastore-side 401 itself is a server-side issue on metastore.europe-west3.gcp.keboola.com
and is not addressed here — this PR makes it diagnosable rather than misattributed.

How it was tested

New TestUnauthorizedErrorMapping in tests/test_http_base.py (9 cases), covering the reported
Metastore body, exceptionId survival on 401/403/404, both regression guards (a genuine
Invalid access token, and expiry phrasing that never says "token"), token masking, and the
empty-body rule.

A/B verified the tests are not vacuous: with src/keboola_agent_cli/http_base.py stashed, the 4
new-behaviour tests fail and the 4 regression guards still pass.

make check exits 0 — 6369 passed, lint/format clean, check_error_codes.py confirms
docs/error-codes.md matches the enum, check_version_gates.py clean.

Doc surfaces

  • docs/error-codes.md — new AUTH_REJECTED row (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.
  • No CLI command added / renamed / removed, so the convention v0.6.0: Branch lifecycle management + security hardening #17 command surfaces are untouched.
  • No version bump and no changelog.py entry — that belongs to the release PR.

Fixes #711

padak added 2 commits August 29, 2026 07:18
`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).
@padak

padak commented Aug 30, 2026

Copy link
Copy Markdown
Member Author

Verification + root cause found — pushed a follow-up commit

I 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 AUTH_REJECTED. Pushed a second commit implementing it on top of this branch.

Root cause (server-side, by design)

go-monorepo services/metastore/internal/middleware/auth.go, resolveProjectScope: any error from dependencies.NewProjectDeps is logged server-side and replaced with the opaque 401 "Failed to create project scope". The middleware calls NewProjectDeps without WithoutMasterToken(), so newProjectScope unconditionally rejects any non-master token with MasterTokenRequiredError — whose actionable message ("Please provide a master token of a project administrator.") is discarded.

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 (is_master_token: false is right in the issue report), so this is not a stack/org outage and not something to escalate — it is the metastore's auth gate by design. (Independent corroboration: keboola/agnes-the-ai-analyst connectors/keboola/semantic_layer.py hit the same wall on 2026-07-15 and added a client-side isMasterToken pre-flight for exactly this reason.)

Live A/B verification (us-east4.gcp, same stack, same commands)

token result
non-master (crm-test, project 5510) 401 Failed to create project scope on every semantic-layer call
master (po, project 5957) metastore auth passes (legitimate CONFIG_ERROR: Project has no semantic-layer models)

Also reproduced on europe-west3.gcp in a different org (425) than the reporter's (343) with a non-master token — confirming it is not org-specific.

Follow-up commit

MetastoreClient now funnels every request through a reclassification: 401 + "Failed to create project scope"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:

The Metastore API (semantic layer) rejected the request with HTTP 401 'Failed to create
project scope'. Unlike the Storage API, the metastore accepts only a MASTER (project admin)
Storage token, and this is how it answers a valid non-master token (token: 5510-...Fytq).
Check `kbagent project info` -> is_master_token, and register a master token
(`kbagent project edit --token ...`) to use semantic-layer commands. [exceptionId: metastore-...]

The generic AUTH_REJECTED mapping from the first commit stays — it now covers other unexplained 401s (where "verify, then escalate with the exceptionId" remains the right advice); 401s that do blame the credential keep INVALID_TOKEN. Docs updated accordingly (error-codes.md, gotchas.md, commands-reference.md, semantic-layer-workflow.md, keboola-expert.md, CLAUDE.md, kbagent context), plus the stale "semantic-layer is a gated feature" claim in tests/helpers.py:metastore_scope_available corrected.

Verified live on the branch: non-master token now gets MISSING_MASTER_TOKEN + remedy + exceptionId, exit 3; master token unaffected. Full test suite green locally.

Worth considering upstream (out of scope here): the metastore middleware could surface MasterTokenRequiredError's own message instead of the blanket wrapper — that would have made this a non-issue for every client.

@linear-code

linear-code Bot commented Aug 30, 2026

Copy link
Copy Markdown

PSGO-282

@padak
padak marked this pull request as ready for review August 30, 2026 11:58
@padak
padak merged commit 17f04fa into main Aug 30, 2026
4 checks passed
@padak
padak deleted the claude/issue-711-401-error-mapping branch August 30, 2026 11:58
@padak padak mentioned this pull request Aug 30, 2026
9 tasks
padak added a commit that referenced this pull request Aug 30, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant