Skip to content

feat(api)!: send route_not_found and authentication_unavailable - #672

Merged
ThomasK33 merged 3 commits into
mainfrom
issue352-specific-codes
Nov 4, 2026
Merged

ThomasK33 merged 3 commits into
mainfrom
issue352-specific-codes

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

open.storage now sends two more of upstream's specific error codes. Each needed a status or shape change, which is why the earlier #352 PRs left them out. This is the rest of item 1 of #352, and the last PR for that issue.

What changes

Case Before After (as upstream documents)
An /api/ path that no route matches 404 text/plain "404 page not found" (ServeMux) 404 problem body, route_not_found
An /api/ path that has routes, but not for this method 405 text/plain, with Allow 405 problem body, method_not_allowed, with the same Allow
The key store fails while the server verifies a token 401 unauthorized "invalid token" 403 authentication_unavailable "cannot verify the token now, retry"
The key store fails while the server verifies a fork's base_repo.auth.token 403 forbidden "base_repo.auth.token: invalid token" 403 authentication_unavailable

How:

  1. unmatchedAPIRoute (api.go) is registered for /api/. Registering it stops ServeMux from writing its own 405s, so it asks the mux which pattern each other method would match and builds Allow from that.
  2. auth.OrgKeyVerifier.Verify returns a *KeySourceError when the KeySource fails. The middleware answers 403 authentication_unavailable unless the error is meta.ErrNotFound (an unknown issuer stays 401). Verify now refuses an iss with a NUL before the key lookup, because a NUL fails the PostgreSQL query, so a crafted iss cannot turn into "retry". Such tokens were 401 before and stay 401. Other control characters still reach the key lookup, because a configured org may hold them (Codex round 2).
    Codex round 1: the fork handler verifies base_repo.auth.token itself, so it now uses the same check (keyStoreFailed). The Git and LFS transports send no problem bodies, so they keep 401, and authentication.md says so.
  3. Docs: http-api.md (Errors, status table), authentication.md (failure map), and the RFC 9457 row of wire-compatibility.md.

Upstream evidence: the vendored spec's "Specific codes" list. The live server answers GET /api/nope with 404 route_not_found and DELETE /api/meta with 405 method_not_allowed and Allow: GET, HEAD, both application/problem+json.

Codes that stay unsent, with the reason

While checking item 1 I found that upstream answers an invalid or expired token with 403 forbidden, not 401. The vendored spec does not say this, so I filed it as #664 instead of changing it here.

Validation

  • go build ./..., golangci-lint run (2.15.0): 0 issues. Docs gate: 0 broken links.
  • go test ./internal/api ./internal/auth ./cmd/... pass. New tests: TestUnmatchedAPIRoutes (both lanes), TestAuthenticationUnavailable, TestVerify_KeySourceError.
  • Race lanes on this head: see the comment below.

Dogfood

I used the verify-openstorage harness on a node built from the item 3 part 3 head (0244c633) and on this change (4343a41a; the head adds only a comment and a doc note from part 3). Each run passed 8 checks. The token without a repo claim was signed with PyJWT and the run's org key, because openstorage mint requires --repo.

Request Before After
GET /api/nope, GET /api/repos/demo/nope 404 text/plain 404 route_not_found
DELETE /api/meta 405 text/plain, Allow: GET, HEAD 405 method_not_allowed, Allow: GET, HEAD
GET /api/v1/repos/commit-pack 405 text/plain, Allow: POST 405 method_not_allowed, Allow: POST
GET /api/v1/repo with no repo claim 403 forbidden 403 forbidden (unchanged, see repo_claim_missing above)
GET /api/v1/repo with a valid token, and with none 200, 401 200, 401 (unchanged)

SDK 0.1.0 Go: repos.list succeeds on both nodes, because #662 serves GET /api/repos. No SDK 0.1.0 call I tried reaches an unmatched route. I could not produce a key store failure on a real node, so the unit test covers authentication_unavailable.

Fixes #352

BREAKING CHANGE: a key store failure during REST token verification answers 403 authentication_unavailable instead of 401 (or instead of 403 forbidden for a fork source token; the Git and LFS transports keep 401; during a key store outage a caller with any well-formed token sees this 403 instead of 401), and unmatched /api/ paths answer problem bodies (404 route_not_found, or 405 method_not_allowed with Allow) instead of ServeMux's plain text.


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

Two of upstream's specific codes needed a status or shape change:

- An /api/ path that no route matches answers 404 route_not_found as a
  problem body, and a path with routes for other methods answers 405
  method_not_allowed with Allow, instead of ServeMux's plain text.
- A key store failure while verifying a token answers 403
  authentication_unavailable instead of 401 invalid token. The verifier
  now returns a KeySourceError and refuses an iss with a control
  character before the key lookup.

repo_claim_missing stays unsent: since #666 a token without a repo
claim is org-wide, and a per-repo route refuses it with 403, as it
refuses repo "org".

_Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high_

Change-Id: Id04ba9ebfd98048895dff2aca9070fbeb5b36120
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33

Copy link
Copy Markdown
Member Author

Validation evidence for this head (f8e05ad4). The item 1 diff is identical on every head below (I compared the changed lines after each rebase).

Head Check Result
882fe8d9 go test -race -count=1 -tags=openstorage_viewcheck ./internal/api/... ./internal/auth/... ./cmd/..., and ./internal/githttp on PostgreSQL pass
882fe8d9 ./internal/githttp on the local backend, -race hit the 15-minute lane limit (the running test was 2 s old, host load about 40)
6aefd1c7 ./internal/githttp on the local backend, -race, 30-minute limit pass in 773 s
f8e05ad4 go build ./..., golangci-lint run ./... (0 issues), docs gate (0 broken links), go test ./internal/api ./internal/auth ./cmd/... -count=1 pass

Dogfood ran on 4343a41a (this change) and 0244c633 (its base), 8 of 8 checks each. The code under test is the same as this head.

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-04T20:44:24.373233Z 14c9841 Manual request
🔒 Security Review ✅ Completed 2026-11-04T20:46:43.717975Z 14c9841 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: f8e05ad483

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f8e05ad483

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/api/middleware.go Outdated
Comment on lines +128 to +129
var keysErr *auth.KeySourceError
if errors.As(err, &keysErr) && !errors.Is(err, meta.ErrNotFound) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Map key-source failures on every token surface

When the key store starts failing after the outer create-repository authorization succeeds, forkAwareCreateRepo calls Verifier.Verify directly in internal/api/handlers_fork.go and still converts the resulting KeySourceError to a generic 403; Git and LFS authentication likewise converts it to 401 in internal/githttp/server.go. Consequently the new authentication_unavailable retry signal applies only to REST bearer middleware even though docs/reference/authentication.md includes fork-source and Git transports in the same failure map. Route these direct verification consumers through the same key-source-error classification.

Useful? React with 👍 / 👎.

…entication_unavailable

Codex round 1 on #672: forkAwareCreateRepo verified base_repo.auth.token
itself and answered every failure 403 forbidden. A key store failure
there is now 403 authentication_unavailable, as in requireAuth, through
one keyStoreFailed helper. The failure map says the Git and LFS
transports keep 401: they send no problem bodies.

Change-Id: I7d39a65ad8a370f0f2ebee4eb7416cdbf7d51dac
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33

Copy link
Copy Markdown
Member Author

Codex round 1 had one P2 finding (medium severity): the key store mapping covered only the REST bearer middleware. I fixed it in f35e2ad, a new commit (no force-push):

  1. Fork source tokens: forkAwareCreateRepo verifies base_repo.auth.token itself. A key store failure there now answers 403 authentication_unavailable ("cannot verify base_repo.auth.token now, retry"), instead of 403 forbidden "invalid token". requireAuth and the fork path share one keyStoreFailed helper. TestForkSourceKeyStoreFailure covers it on both backends. It fails without the fix.
  2. Git and LFS transports: I did not change them. authentication_unavailable is a problem-details code, and the Git transport sends no problem bodies. The failure map in authentication.md now says those transports keep 401.

go test ./internal/api -count=1 passes, golangci-lint reports 0 issues, and the docs gate reports 0 broken links.

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f35e2ad02f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/auth/verifier.go Outdated
Comment on lines +208 to +209
if strings.ContainsFunc(unverified.Iss, unicode.IsControl) {
return nil, errors.New("jwt iss claim has a control character")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject control-character orgs at configuration time instead

When OPENSTORAGE_ORG contains a control character other than PostgreSQL's problematic NUL, the server currently accepts and persists that value—resolveServeConfig passes it through verbatim and the metadata backends impose no such restriction—but this new check rejects every correctly signed token for the configured issuer before lookup. Because the verifier is shared by REST, Git, and LFS, such a deployment boots successfully yet cannot authenticate any request; either validate the configured org at startup or narrow this token-side rejection to values that can actually make the lookup fail.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: f35e2ad02f

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

Codex round 2 on #672: refusing every control character would lock out
a configured org that holds one. Only a NUL fails the PostgreSQL
lookup, and no configured org can hold one.

Change-Id: I16997fe2a074968d1d81345a1ad70202027096af
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33

Copy link
Copy Markdown
Member Author

Codex round 2 had one P2 finding (medium severity): refusing every control character in iss would lock out a configured OPENSTORAGE_ORG that holds one. 14c9841 (new commit, no force-push) refuses only a NUL, the one character that fails the PostgreSQL lookup. An environment variable cannot hold a NUL, so no configured org is affected. TestVerify_KeySourceError now checks that an iss with a tab reaches the key lookup. go test ./internal/api ./internal/auth ./cmd/... -count=1 passes, and golangci-lint reports 0 issues.

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: 14c98410b6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 14c98410b6

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

api: problem-details later tiers (specific codes, instance, result envelope, git/LFS writers, problem+json) — held on SDK trigger

1 participant