fix(measurements): name the batch validation refusal and the band an entry broke

The validation 422 from `POST /api/measurements/batch` now carries
`meta.errorCode: "measurement.batch.invalid"`, beside the existing
`too_large` and `source_not_permitted` codes, so a client can file the
rows as permanently refused instead of holding its anchor on a guess.

A `value_out_of_range` entry now also carries `convertedValue`, the
reading after conversion into the stored unit, and `range`
(`min`, `max`, `unit`), the band it was checked against. Both appear
only on that skip. A fraction sent where a percent was expected is now
visible in the response instead of looking like a sensor glitch.

Status codes, the envelope and every existing field are unchanged.
This commit is contained in:
Marc-André Bombeck committed 2026-09-25 07:39:07 +02:00
1 parent 74dedee645
commit a7682a9fed
5 files changed
+320 -15

No files matched your search

+27 -3
View File
@@ -3936,9 +3936,11 @@ paths:
"401": *a1
"409": *a7
"422":
description: The batch exceeded the 500-entry limit (`measurement.batch.too_large`), failed validation, or
description: "The batch exceeded the 500-entry limit (`measurement.batch.too_large`), failed validation
(`measurement.batch.invalid`, with every issue under `details.issues`), or
(`measurement.batch.source_not_permitted`) carried an entry naming `APPLE_HEALTH` under a narrow
`measurements:write` credential. Nothing was written in any of the three cases.
`measurements:write` credential. Nothing was written in any of the three cases, and all three are permanent
for the batch as sent: resending it unchanged is refused the same way."
content:
application/json:
schema:
@@ -30284,7 +30286,7 @@ components:
labs.update.linkedFieldsImmutable, labs.update.numericExpected, labs.update.qualitativeExpected,
labs.update.referenceRangeInvalid, labs.update.sourceReferenceRangeInvalid; managed_profile —
managed_profile.guardian.duplicate, managed_profile.guardian.managed_invitee,
managed_profile.guardian.required, managed_profile.not_found; measurement —
managed_profile.guardian.required, managed_profile.not_found; measurement — measurement.batch.invalid,
measurement.batch.source_not_permitted, measurement.batch.too_large, measurement.bulk-delete.invalid,
measurement.create.invalid, measurement.create.source_not_permitted, measurement.delete.too_large,
measurement.duplicate_timestamp, measurement.restore.invalid, measurement.update.server_owned_source;
@@ -39736,6 +39738,28 @@ components:
not advance the entry cursor; the response is marked `Cache-Control: no-store`."
reason:
type: string
convertedValue:
description: "Only on a `value_out_of_range` skip (since v1.39.1): the entry's value after conversion into the stored
unit, which is the number the band was checked against. A HealthKit fraction that should have been a percent
shows up here a hundred times off."
type: number
range:
description: "Only on a `value_out_of_range` skip (since v1.39.1): the plausibility band that refused the entry, in the
stored unit. Both edges are inclusive. A skip is permanent for the value as sent; resending it unchanged
skips it again."
type: object
properties:
min:
type: number
max:
type: number
unit:
type: string
required:
- min
- max
- unit
additionalProperties: false
required:
- index
- status