You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit d31be92
Browse filesBrowse the repository at this point in the historyBrowse files
chore(metadata-protocol): pin new engine doubles + doc the new 400 + adr-0087 marker (#13576)
- scripts/engine-double-contract.pinned.json: register the fake engine
double introduced by protocol.occ-empty-etag-rejected.test.ts
(node scripts/check-engine-double-contract.mjs --write).
- content/docs/api/wire-format.mdx: document the new 400
VALIDATION_FAILED refusal for the quoted-empty If-Match entity-tag,
alongside the existing OCC/409 documentation.
- .changeset/*.md: add the required ADR-0087 disposition marker
(not-required / no-migration-prescription) for the declared-breaking
changeset.
Copy file name to clipboardExpand all lines: .changeset/occ-empty-etag-rejected-at-ingress.md
+3Lines changed: 3 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -57,3 +57,6 @@ truthy — an empty value never reaches the wire on any first-party path. The
57
57
exposure was to third-party and hand-rolled clients sending the RFC-7232
58
58
empty-tag shape, which previously got an unguarded write where they asked for
59
59
a guarded one.
60
+
61
+
<!-- adr-0087: not-required (no-migration-prescription) no metadata key, spec symbol, or stored value is renamed/retired/converted — this narrows what a REQUEST-time client-supplied string (`expectedVersion`/`If-Match`) is accepted at the wire ingress, not any declared metadata surface `objectstack migrate meta` would touch -->
Copy file name to clipboardExpand all lines: content/docs/api/wire-format.mdx
+20-2Lines changed: 20 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -198,7 +198,7 @@ Updates specific fields on an existing record. Only include fields you want to c
198
198
```
199
199
200
200
<Callouttype="info">
201
-
**Optimistic concurrency:** Pass the `updated_at` value you last read as an `If-Match` request header (or an `expectedVersion` field in the body) and the server returns `409 CONCURRENT_UPDATE` if the record changed in the meantime.
201
+
**Optimistic concurrency:** Pass the `updated_at` value you last read as an `If-Match` request header (or an `expectedVersion` field in the body) and the server returns `409 CONCURRENT_UPDATE` if the record changed in the meantime. Omitting `If-Match`/`expectedVersion` entirely performs an unguarded write. Sending the empty entity-tag `If-Match: ""` (or `expectedVersion: '""'`) is refused `400 VALIDATION_FAILED` — an empty token can never match any stored version, so it is treated as a client defect rather than either "no guard requested" or a real conflict.
202
202
</Callout>
203
203
204
204
### Response — `200 OK`
@@ -378,6 +378,24 @@ Returned when an `If-Match` / `expectedVersion` token no longer matches the stor
378
378
}
379
379
```
380
380
381
+
### Malformed Concurrency Token — `400 Bad Request`
382
+
383
+
Returned when `If-Match` / `expectedVersion` is the empty entity-tag `""` — a
token, but one that can never match a stored version. Distinct from both the
386
+
409 above (a real token that lost a race) and an omitted `If-Match` (a
387
+
deliberate unguarded write): sending `""` is treated as a client defect, since
388
+
no stored version can ever equal "nothing".
389
+
390
+
```json
391
+
{
392
+
"error": "expectedVersion (If-Match) is the empty entity-tag \"\". An empty version token can never match any stored version, so this is almost certainly a client defect rather than a real concurrency check — send the real version token you read (e.g. the record's `updated_at`), or omit If-Match / expectedVersion entirely to perform an unguarded write.",
393
+
"code": "VALIDATION_FAILED",
394
+
"fields": [],
395
+
"object": "task"
396
+
}
397
+
```
398
+
381
399
### Datasource Unavailable — `503 Service Unavailable`
382
400
383
401
Returned when the object's declared `datasource` has no live driver: the host's
@@ -543,7 +561,7 @@ When the batch is not atomic and some records fail, each failing entry carries a
543
561
|`Content-Type`| Yes |`application/json`|
544
562
|`X-Request-Id`| No | Client-generated request ID for tracing (honored by the observability dispatcher) |
545
563
|`X-Environment-Id`| No | Targets a specific environment/project on unscoped routes |
546
-
|`If-Match`| No | Optimistic-concurrency token for `PATCH` / `DELETE` (the `updated_at` you last read) |
564
+
|`If-Match`| No | Optimistic-concurrency token for `PATCH` / `DELETE` (the `updated_at` you last read). Omit for an unguarded write; the empty entity-tag `""` is refused `400`, not treated as omitted.|
547
565
|`Accept-Language`| No | Locale for translated labels (e.g., `en-US`) |
0 commit comments