mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
feat(api): give every error envelope a meta.errorCode
Roughly half of the refusals answered prose alone, so a client could branch on the code for one 404 and had to fall back to the status for the next. apiHandler now completes every error envelope with the status's generic http.* code when the route named none; a route's own code still wins. The family joins the published catalogue, and the profile form only shows a code's sentence when the code has one.
This commit is contained in:
1 parent
c7f69fc212
commit
fbbb535832
11 files changed
+219
-50
No files matched your search
@@ -211,6 +211,25 @@ export const ERROR_CODE_CATALOGUE: Readonly<Record<string, readonly string[]>> =
|
||||
"health_score_config.invalid",
|
||||
"health_score_config.too_narrow",
|
||||
],
|
||||
http: [
|
||||
"http.bad_gateway",
|
||||
"http.bad_request",
|
||||
"http.client_error",
|
||||
"http.conflict",
|
||||
"http.forbidden",
|
||||
"http.gateway_timeout",
|
||||
"http.gone",
|
||||
"http.internal",
|
||||
"http.invalid",
|
||||
"http.method_not_allowed",
|
||||
"http.not_found",
|
||||
"http.payload_too_large",
|
||||
"http.rate_limited",
|
||||
"http.server_error",
|
||||
"http.unauthenticated",
|
||||
"http.unavailable",
|
||||
"http.unsupported_media_type",
|
||||
],
|
||||
illness: [
|
||||
"illness.day-log.invalid",
|
||||
"illness.disabled",
|
||||
|
||||
@@ -85,7 +85,7 @@ export const errorEnvelope = z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Stable machine code for this refusal. Branch on it rather than on `error`, which is prose and may be reworded. Every code the API emits today is enumerated below, grouped by the surface that emits it; four naming conventions coexist and none of them will be renamed, because a code is a wire value a shipped client branches on. Treat an unlisted code the way you would treat an unlisted enum member — as a refusal you do not recognise, not as a malformed response — since the list grows with the surfaces. Three families are outside it on purpose: `assistant.disabled.<switch>` is built from a template so the last segment is open — it is one of `coach`, `briefing`, `insightStatus`, `documentAi` and `enabled`, and `assistant.disabled.coach` is the one the native client names; the integration-probe classes (`credentials_rejected`, `rate_limited`, `upstream_error`, `timeout`, `connection_failed` and the per-provider additions) are enumerated in each `/test` operation instead, where the differences can be stated; and a 401 raised by a route checking a credential of its own may carry no code at all. " +
|
||||
"Stable machine code for this refusal. Branch on it rather than on `error`, which is prose and may be reworded. Every code the API emits today is enumerated below, grouped by the surface that emits it; four naming conventions coexist and none of them will be renamed, because a code is a wire value a shipped client branches on. Every error envelope carries a code: when the route has no reason of its own to name, the code is the status's generic one from the `http` family (`http.not_found` for a 404, `http.internal` for a 500, `http.client_error` / `http.server_error` for a status without its own entry), so `http.*` says which kind of refusal it was and never why. Treat an unlisted code the way you would treat an unlisted enum member — as a refusal you do not recognise, not as a malformed response — since the list grows with the surfaces. Three families are outside it on purpose: `assistant.disabled.<switch>` is built from a template so the last segment is open — it is one of `coach`, `briefing`, `insightStatus`, `documentAi` and `enabled`, and `assistant.disabled.coach` is the one the native client names; the integration-probe classes (`credentials_rejected`, `rate_limited`, `upstream_error`, `timeout`, `connection_failed` and the per-provider additions) are enumerated in each `/test` operation instead, where the differences can be stated; and a 401 raised by a route checking a credential of its own carries the generic `http.unauthenticated`. " +
|
||||
renderErrorCodeCatalogue(),
|
||||
),
|
||||
capability: z
|
||||
@@ -118,7 +118,7 @@ export const errorEnvelope = z
|
||||
.meta({
|
||||
id: "ErrorEnvelope",
|
||||
description:
|
||||
"Standard error response: `data` is null, `error` is human prose. `meta.errorCode` carries the stable machine code where one exists, and `meta` may carry further per-refusal context. A 422 from the multi-issue validator additionally carries `details.issues` — every field the request got wrong, so a client can fix them in one round-trip.",
|
||||
"Standard error response: `data` is null, `error` is human prose. `meta.errorCode` always carries a stable machine code — the route's own where it names one, otherwise the status's generic `http.*` code — `meta.requestId` names the request for a support conversation, and `meta` may carry further per-refusal context. A 422 from the multi-issue validator additionally carries `details.issues` — every field the request got wrong, so a client can fix them in one round-trip.",
|
||||
});
|
||||
|
||||
export function dataEnvelope<T extends z.ZodType>(payload: T, id: string) {
|
||||
@@ -279,7 +279,7 @@ export const malformedJsonResponse = {
|
||||
export const stdResponses = {
|
||||
"401": {
|
||||
description:
|
||||
"Authentication required or invalid credentials. The generic auth gates — the ones every route passes through before its own body runs — name the reason in `meta.errorCode`, and that is the field to branch on there; the English sentence beside it may be reworded or localised at any time. `auth.missing`: no session cookie and no Bearer header; send the user to sign in. `auth.token.expired`: the Bearer token's lifetime has passed; refresh and retry the request. `auth.token.invalid`: unknown, revoked, or the owning account is gone; discard the credential and sign in again — retrying will not help. `auth.mfa.code_invalid`: a second factor presented on a step-up flow did not verify; re-prompt for a code and keep the session. The related 403 refusals carry `auth.scope.insufficient` (the credential is valid but its scope does not reach this route, including any Bearer token against an admin surface) and `auth.admin.required` (a cookie session that is not an admin's). A route that checks a credential of its own — sign-in, the MFA challenge exchange, a password confirmation — may still answer 401 with prose and no code, so a missing `meta.errorCode` means the route refused the credential it was handed, not that the response is malformed. A session that could not be checked because the session store was unreachable is NOT a 401: it answers 503 with `auth.session.unavailable`, and the client keeps its credential and retries.",
|
||||
"Authentication required or invalid credentials. The generic auth gates — the ones every route passes through before its own body runs — name the reason in `meta.errorCode`, and that is the field to branch on there; the English sentence beside it may be reworded or localised at any time. `auth.missing`: no session cookie and no Bearer header; send the user to sign in. `auth.token.expired`: the Bearer token's lifetime has passed; refresh and retry the request. `auth.token.invalid`: unknown, revoked, or the owning account is gone; discard the credential and sign in again — retrying will not help. `auth.mfa.code_invalid`: a second factor presented on a step-up flow did not verify; re-prompt for a code and keep the session. The related 403 refusals carry `auth.scope.insufficient` (the credential is valid but its scope does not reach this route, including any Bearer token against an admin surface) and `auth.admin.required` (a cookie session that is not an admin's). A route that checks a credential of its own — sign-in, the MFA challenge exchange, a password confirmation — may answer 401 with the generic `http.unauthenticated`, which means the route refused the credential it was handed. A session that could not be checked because the session store was unreachable is NOT a 401: it answers 503 with `auth.session.unavailable`, and the client keeps its credential and retries.",
|
||||
content: { "application/json": { schema: errorEnvelope } },
|
||||
},
|
||||
"422": {
|
||||
|
||||
Reference in new issue
Block a user