fix(dwctl): point unknown-API-key 401s at the regional-endpoints docs - #1541
Conversation
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
Deploying control-layer with
|
| 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 |
There was a problem hiding this comment.
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_MESSAGEconstant to keep the 401 copy consistent across surfaces. - Updates the AI models listing endpoint and the
CurrentUserextractor 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
| // 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 }) |
There was a problem hiding this comment.
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.
| 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\", |
There was a problem hiding this comment.
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.
| assert!( | ||
| !text.contains("api.doubleword.ai"), | ||
| "401 copy must not enumerate regional base URLs" | ||
| ); | ||
| } |
There was a problem hiding this comment.
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
What
Rewrites the
Invalid API key401 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:Status code (401) and error schema (
authentication_error/invalid_api_key) are unchanged.Where
dwctl/src/errors.rs: new sharedINVALID_API_KEY_MESSAGEconstant so the copy cannot drift between surfaces.dwctl/src/api/handlers/ai_models.rs: theGET /ai/v1/modelsunknown-key 401 (the first endpoint people hit when smoke-testing a key).dwctl/src/auth/current_user.rs: theCurrentUserextractor'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