mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
docs(api): publish details.issues and open meta on the error envelope
Every documented 422, 401, 403, 409, 410 and 429 points at one `ErrorEnvelope`, and that schema declared `data`, `error` and a `meta` closed to `requestId` + `errorCode`. Two things were wrong with it. `returnAllZodIssues` has sent a third top-level key since v1.4.42 — `details.issues`, one entry per rejected field — from 166 route files. It exists so a client can fix three bad fields in one round-trip instead of three, and the doc comment says so, yet no component schema anywhere declared it. A client generated from the contract had no field to read it from, so the feature was invisible to exactly the consumer it was built for. It is now a schema, with the `unrecognized_keys` list and the promise that no rejected value is echoed back. And `meta` was closed while real refusals put more in it: `removedIn` / `replacedBy` on a 410, `module` beside `module.disabled`, the per-integration context on a failed connection test. Closed plus those keys means the response was invalid against its own published schema and a strict decoder is entitled to reject it. It is open now. No wire change; both halves describe what the server already sends.
This commit is contained in:
1 parent
1d2744a109
commit
fd8fc0ec0e
2 files changed
+105
-5
No files matched your search
+46
-2
@@ -29018,19 +29018,63 @@ components:
|
||||
type: "null"
|
||||
error:
|
||||
type: string
|
||||
details:
|
||||
description: "Present on a 422 raised by the multi-issue validator: every field the request got wrong, not only the
|
||||
first. Absent on every other status."
|
||||
type: object
|
||||
properties:
|
||||
issues:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/ValidationIssue"
|
||||
required:
|
||||
- issues
|
||||
additionalProperties: false
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
errorCode:
|
||||
description: Stable machine code for this refusal. Branch on it rather than on `error`, which is prose and may be
|
||||
reworded.
|
||||
type: string
|
||||
additionalProperties: false
|
||||
additionalProperties: {}
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
description: "Standard error response: data is null, error is human prose."
|
||||
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."
|
||||
ValidationIssue:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: Dot-joined path to the rejected field, e.g. `entries.3.measuredAt`. Empty string when the whole body was
|
||||
rejected.
|
||||
code:
|
||||
type: string
|
||||
description: Zod issue code, e.g. `invalid_type`, `too_small`, `unrecognized_keys`. Treat an unfamiliar code as a
|
||||
generic rejection of `path`.
|
||||
message:
|
||||
type: string
|
||||
description: English sentence describing the rejection. Safe to log; show the user your own wording keyed on `path`.
|
||||
keys:
|
||||
description: "Only on `unrecognized_keys`: the refused key names, bounded to 10 and 64 characters each. Zod reports
|
||||
every unknown key of one object in a single issue whose `path` is the object, so the names live here and
|
||||
nowhere in `path`."
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
required:
|
||||
- path
|
||||
- code
|
||||
- message
|
||||
additionalProperties: false
|
||||
description: "One rejected field in a multi-issue 422. Sanitised: no rejected VALUE is echoed back."
|
||||
CycleCustomSymptomCreatedEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -21,20 +21,76 @@ import { createBatchWorkoutSchema as createBatchWorkoutSchemaBase } from "@/lib/
|
||||
* `{ data, error, meta? }`. The OpenAPI surface mirrors that contract
|
||||
* so iOS / external-ingest clients can decode uniformly.
|
||||
*/
|
||||
/**
|
||||
* One rejected field from a multi-issue 422.
|
||||
*
|
||||
* `returnAllZodIssues` has emitted this since v1.4.42 so a client can fix
|
||||
* three bad fields in one round-trip instead of three, and 166 route files
|
||||
* send it — but no component schema declared it, so the very consumer it was
|
||||
* built for could not read it from the contract. `params` never ships: some
|
||||
* Zod codes embed the offending value in it and that is user content.
|
||||
*/
|
||||
const validationIssue = z
|
||||
.object({
|
||||
path: z
|
||||
.string()
|
||||
.describe(
|
||||
"Dot-joined path to the rejected field, e.g. `entries.3.measuredAt`. Empty string when the whole body was rejected.",
|
||||
),
|
||||
code: z
|
||||
.string()
|
||||
.describe(
|
||||
"Zod issue code, e.g. `invalid_type`, `too_small`, `unrecognized_keys`. Treat an unfamiliar code as a generic rejection of `path`.",
|
||||
),
|
||||
message: z
|
||||
.string()
|
||||
.describe(
|
||||
"English sentence describing the rejection. Safe to log; show the user your own wording keyed on `path`.",
|
||||
),
|
||||
keys: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe(
|
||||
"Only on `unrecognized_keys`: the refused key names, bounded to 10 and 64 characters each. Zod reports every unknown key of one object in a single issue whose `path` is the object, so the names live here and nowhere in `path`.",
|
||||
),
|
||||
})
|
||||
.meta({
|
||||
id: "ValidationIssue",
|
||||
description:
|
||||
"One rejected field in a multi-issue 422. Sanitised: no rejected VALUE is echoed back.",
|
||||
});
|
||||
|
||||
export const errorEnvelope = z
|
||||
.object({
|
||||
data: z.null(),
|
||||
error: z.string(),
|
||||
details: z
|
||||
.object({ issues: z.array(validationIssue) })
|
||||
.optional()
|
||||
.describe(
|
||||
"Present on a 422 raised by the multi-issue validator: every field the request got wrong, not only the first. Absent on every other status.",
|
||||
),
|
||||
// Loose on purpose. Real refusals put more than the two named keys here —
|
||||
// `removedIn` / `replacedBy` on a 410, `module` beside `module.disabled`,
|
||||
// the per-integration context on a failed connection test — and a closed
|
||||
// object made the body invalid against its own published schema, which any
|
||||
// strict generated decoder is entitled to reject.
|
||||
meta: z
|
||||
.object({
|
||||
.looseObject({
|
||||
requestId: z.string().optional(),
|
||||
errorCode: z.string().optional(),
|
||||
errorCode: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Stable machine code for this refusal. Branch on it rather than on `error`, which is prose and may be reworded.",
|
||||
),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.meta({
|
||||
id: "ErrorEnvelope",
|
||||
description: "Standard error response: data is null, error is human prose.",
|
||||
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.",
|
||||
});
|
||||
|
||||
export function dataEnvelope<T extends z.ZodType>(payload: T, id: string) {
|
||||
|
||||
Reference in new issue
Block a user