mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +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
+38
-29
@@ -80,10 +80,10 @@ paths:
|
||||
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."
|
||||
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:
|
||||
@@ -36097,15 +36097,19 @@ components:
|
||||
description: "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. ai —
|
||||
ai.budget.exceeded, ai.provider.none, ai.record.notPermitted, ai.unavailable, ai.usage.aboveCeiling,
|
||||
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`. ai — ai.budget.exceeded,
|
||||
ai.provider.none, ai.record.notPermitted, ai.unavailable, ai.usage.aboveCeiling,
|
||||
ai.usage.defaultAboveCeiling, ai.usage.invalid, ai.usage.levelTooLow, ai.usage.rateLimited; ai_provider
|
||||
— ai_provider.invalid, ai_provider.no_fields; aiRuns — aiRuns.failed, aiRuns.notFound, aiRuns.timedOut,
|
||||
aiRuns.workerUnavailable; allergy — allergy.invalid; analytics — analytics.invalid_query; anamnesis —
|
||||
@@ -36152,18 +36156,22 @@ components:
|
||||
environment.range_too_large, environment.travel.not_found, environment.travel_suggestion.not_found;
|
||||
export — export.selection.empty, export.selection.unknown_leaf; familyHistory — familyHistory.invalid;
|
||||
feedback — feedback.body.invalid; health_score_config — health_score_config.invalid,
|
||||
health_score_config.too_narrow; illness — illness.day-log.invalid, illness.disabled,
|
||||
illness.episode.chronic-no-resolve, illness.episode.invalid, illness.episode.invalid-window,
|
||||
illness.episode.parent-not-found, illness.insights.invalid; import — import.apple_health.busy,
|
||||
import.write_failed; ingest — ingest.rate_limited; insights — insights.generate.budgetExceeded,
|
||||
insights.generate.outboundScreened, insights.generate.profileScopeChanged,
|
||||
insights.generate.scopeChanged; labs — labs.biomarker.alias.taken, labs.biomarker.create.invalid,
|
||||
labs.biomarker.duplicate, labs.biomarker.merge.invalid, labs.biomarker.merge.unitMismatch,
|
||||
labs.biomarker.notFound, labs.biomarker.update.invalid, labs.biomarker.update.referenceRangeInvalid,
|
||||
labs.create.invalid, labs.link.invalid, labs.ocr.budgetExceeded, labs.ocr.extractFailed,
|
||||
labs.ocr.fileTooLarge, labs.ocr.fileType, labs.ocr.localOcrDisabled, labs.ocr.pdfNeedsAnthropic,
|
||||
labs.ocr.providerUnsupported, labs.ocr.rateLimited, labs.restore.invalid, labs.result.notFound,
|
||||
labs.unit.mismatch, labs.update.invalid, labs.update.linkedFieldsImmutable, labs.update.numericExpected,
|
||||
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, illness.episode.chronic-no-resolve, illness.episode.invalid,
|
||||
illness.episode.invalid-window, illness.episode.parent-not-found, illness.insights.invalid; import —
|
||||
import.apple_health.busy, import.write_failed; ingest — ingest.rate_limited; insights —
|
||||
insights.generate.budgetExceeded, insights.generate.outboundScreened,
|
||||
insights.generate.profileScopeChanged, insights.generate.scopeChanged; labs —
|
||||
labs.biomarker.alias.taken, labs.biomarker.create.invalid, labs.biomarker.duplicate,
|
||||
labs.biomarker.merge.invalid, labs.biomarker.merge.unitMismatch, labs.biomarker.notFound,
|
||||
labs.biomarker.update.invalid, labs.biomarker.update.referenceRangeInvalid, labs.create.invalid,
|
||||
labs.link.invalid, labs.ocr.budgetExceeded, labs.ocr.extractFailed, labs.ocr.fileTooLarge,
|
||||
labs.ocr.fileType, labs.ocr.localOcrDisabled, labs.ocr.pdfNeedsAnthropic, labs.ocr.providerUnsupported,
|
||||
labs.ocr.rateLimited, labs.restore.invalid, labs.result.notFound, labs.unit.mismatch,
|
||||
labs.update.invalid, labs.update.linkedFieldsImmutable, labs.update.numericExpected,
|
||||
labs.update.qualitativeExpected, labs.update.referenceRangeInvalid,
|
||||
labs.update.sourceReferenceRangeInvalid; life_event — life_event.invalid, life_event.not_found;
|
||||
managed_profile — managed_profile.guardian.duplicate, managed_profile.guardian.managed_invitee,
|
||||
@@ -36260,10 +36268,11 @@ components:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
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."
|
||||
description: "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."
|
||||
ValidationIssue:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
Reference in new issue
Block a user