Skip to content

fix(dwctl): point unknown-API-key 401s at the regional-endpoints docs - #1541

Merged
sejori merged 5 commits into
mainfrom
fix/cor-585-wrong-region-401-copy
Aug 29, 2026
Merged

sejori merged 5 commits into
mainfrom
fix/cor-585-wrong-region-401-copy

Conversation

@sejori

@sejori sejori commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

What

Rewrites the Invalid API key 401 copy so a key rejected because it was created in the other region no longer reads as "your key is broken". The message now suggests checking that the request's base URL matches the region the key was created in, and links the regional-endpoints docs page:

Invalid API key. API keys are region-bound: if you expected this key to work, check that your base URL matches the region the key was created in. See https://docs.doubleword.ai/inference-api/regional-endpoints

Status code (401) and error schema (authentication_error / invalid_api_key) are unchanged.

Where

  • dwctl/src/errors.rs: new shared INVALID_API_KEY_MESSAGE constant so the copy cannot drift between surfaces.
  • dwctl/src/api/handlers/ai_models.rs: the GET /ai/v1/models unknown-key 401 (the first endpoint people hit when smoke-testing a key).
  • dwctl/src/auth/current_user.rs: the CurrentUser extractor's unknown-bearer-key rejection, which gates every other dwctl-authenticated surface. The extractor previously accumulated per-method auth errors but always returned a message-less generic 401 ("Authentication required"), so the specific copy never reached the client; it now propagates the first specific failure message (API-key auth is attempted first).
  • dwctl/src/openapi/ai.rs: the documented error example, kept in sync with the live copy.

The realtime proxy path (onwards) is deliberately untouched: it rejects unknown keys with a 403 key-set miss, so changing it would not be a copy-only 401 change.

Notes

The error body intentionally does not enumerate regional base URLs — it stays generic and defers to the docs page, which pairs each key with its base URL. The docs page ships in doublewordai/documentation#58 (held until release).

Tests assert the new copy on both surfaces and that no regional API base URL appears in the error body.

COR-585

A key created in one region is rejected with a bare "Invalid API key" by
the other region's endpoint, which reads as "my key is broken" rather
than "my base URL is wrong". Share one message constant across the
models-list handler and the CurrentUser extractor that suggests checking
the base URL against the key's region and links the docs, without
enumerating endpoint URLs in the error body.

COR-585
Copilot AI lite review requested due to automatic review settings August 28, 2026 14:31
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 28, 2026

Copy link
Copy Markdown

Deploying control-layer with  Cloudflare Pages  Cloudflare Pages

Latest commit: 0b25c08
Status: ✅  Deploy successful!
Preview URL: https://93fa4dcd.control-layer.pages.dev
Branch Preview URL: https://fix-cor-585-wrong-region-401.control-layer.pages.dev

View logs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Updates dwctl’s unknown-API-key 401 messaging to clarify that API keys are region-bound (so a “wrong region” key can look like an invalid key) and to point users to the regional endpoints documentation, while keeping the 401 status code and OpenAI-style error schema unchanged.

Changes:

  • Introduces a shared INVALID_API_KEY_MESSAGE constant to keep the 401 copy consistent across surfaces.
  • Updates the AI models listing endpoint and the CurrentUser extractor to use the shared message for unknown bearer keys.
  • Updates the AI OpenAPI description example and adds tests asserting the new copy and that no regional base URLs are enumerated in the error body.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
dwctl/src/errors.rs Adds shared INVALID_API_KEY_MESSAGE constant for unknown API key 401 copy.
dwctl/src/api/handlers/ai_models.rs Uses the shared message for GET /ai/v1/models unknown-key 401s.
dwctl/src/auth/current_user.rs Uses the shared message when rejecting unknown bearer keys in CurrentUser.
dwctl/src/openapi/ai.rs Updates documented error example message in the AI OpenAPI description.
dwctl/src/test/mod.rs Adds tests covering the new 401 copy on both surfaces and ensuring no base URLs are enumerated.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

… 401s

The CurrentUser extractor accumulated per-method auth errors but always
returned a generic message-less Unauthenticated, so the invalid-API-key
copy never reached the client. Propagate the first specific message
(API-key auth is attempted first) and apply rustfmt to the new tests.

COR-585

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Comment thread dwctl/src/auth/current_user.rs Outdated
Comment on lines +722 to +730
// Surface the most specific failure message rather than the generic
// "Authentication required" fallback. API-key auth is attempted (and
// therefore pushed) first, so an unknown bearer key surfaces its
// region-aware copy even when other credentials also failed.
let message = auth_errors.iter().find_map(|(_, e)| match e {
Error::Unauthenticated { message } => message.clone(),
_ => None,
});
Err(Error::Unauthenticated { message })

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Actioned in 2c12d44, with one clarification: non-Unauthenticated errors were already collapsed into a generic 401 by the pre-existing fallback — this PR did not change that — so (a) predates this change. For (b), agreed: message propagation is now scoped to the API-key method, so every other auth method's response stays byte-identical.

Comment thread dwctl/src/openapi/ai.rs
Comment on lines 330 to 336
Errors follow the OpenAI format with `error.message`, `error.type`, and `error.code` fields:

```json
{
\"error\": {
\"message\": \"Invalid API key\",
\"message\": \"Invalid API key. API keys are region-bound: if you expected this key to work, check that your base URL matches the region the key was created in. See https://docs.doubleword.ai/inference-api/regional-endpoints\",
\"type\": \"authentication_error\",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added in 2c12d44: ai_spec_error_example_matches_live_copy asserts the served /ai/openapi.json embeds INVALID_API_KEY_MESSAGE, so the example cannot silently drift from the live copy.

Comment thread dwctl/src/test/mod.rs
Comment on lines +1904 to +1908
assert!(
!text.contains("api.doubleword.ai"),
"401 copy must not enumerate regional base URLs"
);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added the api.us.doubleword.ai assertion in 2c12d44 (and note the two checks are independent: neither hostname is a substring of the other).

… copy drift

Copilot review follow-ups: restrict the surfaced auth-failure message to
the API-key method so other auth methods' responses stay byte-identical,
assert the AI OpenAPI description embeds the live invalid-API-key copy,
and cover the US hostname in the no-enumeration assertion.

COR-585
@sejori
sejori enabled auto-merge (squash) August 28, 2026 15:05
@sejori
sejori merged commit 6c5b1f9 into main Aug 29, 2026
21 checks passed
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.

2 participants