mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 22:50:51 +00:00
fix(api): answer an unparseable body with 400 everywhere
`safeJson` refuses a body that will not parse with 400, and so does `apiHandler`'s own SyntaxError branch — about two hundred and twenty routes inherit that. Seven `/api/auth/me/*` writes hand-rolled the parse in a try/catch and threw 422 for the identical condition, so a client's "my serializer produced garbage" branch had to accept two statuses on a subset of routes it had no way to predict. All seven answer 400 now, with the sentence `safeJson` already sends and the dotted token each one carried moved to `meta.errorCode`. The 400 is published on those seven operations through a shared response; it is deliberately not folded into the standard set, which is spread onto reads as well and a GET has no body to refuse. `/api/auth/me/health-score-config` keeps its 422: the operation's own description names that status and its exact message, so moving it is a contract decision rather than a cleanup. The guard carries it as an allowlist entry with that reason rather than quietly skipping it. Three route tests pinned the old status; they assert the new one and say why. The guard reads the hand-rolled parses out of the tree and asserts it still finds them, so it cannot pass on a matcher that stopped matching.
This commit is contained in:
1 parent
9ce0bd5027
commit
3e44d68518
15 files changed
+434
-191
No files matched your search
@@ -33,6 +33,7 @@ import {
|
||||
errorEnvelope,
|
||||
idempotencyKeyParameter,
|
||||
idempotentWrite,
|
||||
malformedJsonResponse,
|
||||
recordRefusal,
|
||||
stdResponses,
|
||||
} from "./shared";
|
||||
@@ -991,6 +992,7 @@ export const cyclePaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
},
|
||||
},
|
||||
...stdResponses,
|
||||
...malformedJsonResponse,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -38,16 +38,17 @@ import {
|
||||
healthProfileFactKindSchema,
|
||||
} from "@/lib/validations/health-profile-facts";
|
||||
import {
|
||||
coachPrefsSchema,
|
||||
dataEnvelope,
|
||||
errorEnvelope,
|
||||
stdResponses,
|
||||
baseUpdatedAtField,
|
||||
updatedAtTokenField,
|
||||
coachPrefsSchema,
|
||||
conflictResponse409,
|
||||
invalidBaseTokenResponse,
|
||||
dataEnvelope,
|
||||
deviceRevokeResponse,
|
||||
errorEnvelope,
|
||||
invalidBaseTokenResponse,
|
||||
malformedJsonResponse,
|
||||
recordRefusal,
|
||||
stdResponses,
|
||||
updatedAtTokenField,
|
||||
} from "./shared";
|
||||
|
||||
// v1.18.0 — module enable/disable. The PATCH request is the REAL runtime
|
||||
@@ -1063,6 +1064,7 @@ export const profilePaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
},
|
||||
...conflictResponse409("Coach preferences", "coach_prefs_conflict"),
|
||||
...stdResponses,
|
||||
...malformedJsonResponse,
|
||||
...invalidBaseTokenResponse,
|
||||
},
|
||||
},
|
||||
@@ -1108,6 +1110,7 @@ export const profilePaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
},
|
||||
},
|
||||
...stdResponses,
|
||||
...malformedJsonResponse,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1263,6 +1266,7 @@ export const profilePaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
},
|
||||
...conflictResponse409("Module preferences", "modules_conflict"),
|
||||
...stdResponses,
|
||||
...malformedJsonResponse,
|
||||
...invalidBaseTokenResponse,
|
||||
},
|
||||
},
|
||||
@@ -1310,6 +1314,7 @@ export const profilePaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
},
|
||||
},
|
||||
...stdResponses,
|
||||
...malformedJsonResponse,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1505,6 +1510,7 @@ export const profilePaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
"notification_prefs_conflict",
|
||||
),
|
||||
...stdResponses,
|
||||
...malformedJsonResponse,
|
||||
...invalidBaseTokenResponse,
|
||||
},
|
||||
},
|
||||
@@ -1552,6 +1558,7 @@ export const profilePaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
},
|
||||
},
|
||||
...stdResponses,
|
||||
...malformedJsonResponse,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -225,6 +225,29 @@ export const invalidBaseTokenResponse = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* The 400 an unparseable JSON body earns.
|
||||
*
|
||||
* `safeJson` has answered 400 for a body that will not parse since it was
|
||||
* written, and roughly two hundred and twenty routes go through it — but seven
|
||||
* `/api/auth/me/*` writes hand-rolled the parse and answered 422, so a client's
|
||||
* "my serializer produced garbage" branch had to accept two statuses on a
|
||||
* subset of routes it could not predict. They answer 400 now, and this is the
|
||||
* response that says so. The dotted token each route already carried moved to
|
||||
* `meta.errorCode`, where a machine code belongs.
|
||||
*
|
||||
* Spread only on the operations that hand-rolled the parse. It is deliberately
|
||||
* NOT folded into `stdResponses`: that set is spread onto reads as well, and a
|
||||
* GET that accepts no body has no malformed body to refuse.
|
||||
*/
|
||||
export const malformedJsonResponse = {
|
||||
"400": {
|
||||
description:
|
||||
"The request body is not parseable JSON. Nothing was read and nothing was written. `meta.errorCode` names the surface that refused it (`<surface>.body.invalid_json`). This is a client-side serialisation fault, not a validation failure — a body that parses but fails the schema is the 422 beside this.",
|
||||
content: { "application/json": { schema: errorEnvelope } },
|
||||
},
|
||||
};
|
||||
|
||||
// ── Standard 401 / 422 / 429 responses ───────────────────────────────
|
||||
|
||||
export const stdResponses = {
|
||||
|
||||
Reference in new issue
Block a user