Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion content/docs/api/error-catalog.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,9 +86,48 @@ substitute. See the [Data API](/docs/api/data-api).

### `MISSING_REQUIRED_FIELD`
**Cause:** A required field was not provided in the request body.
**Fix:** Include the missing field. Check `fields` for the field name.
**Fix:** Include the missing field. Check `fields` for the field name — **except** on the
`controlled_by_parent` master-reference paths below, where the status is `422` and no
`fields` array is sent; read the field name out of the message there.
**Retry:** `no_retry`

<Callout type="warn">
**`MISSING_REQUIRED_FIELD` is `400` with one exception: an absent `controlled_by_parent`
master reference answers `422`, without `fields`.**

An object whose `sharingModel` is `controlled_by_parent` derives its access from a master
record, so the gate authorizing writes to it resolves that master *before* the executor —
and the executor is where required-field validation runs. Which of the two refuses first
depends on how the master reference is declared. A `master_detail` that is `required` and
neither `readonly` nor `system` reaches validation and answers the documented
`400 VALIDATION_FAILED` with `fields`. The other four declarable shapes never reach it and
answer `422 MISSING_REQUIRED_FIELD` with no `fields`:

- a `master_detail` with no `required`
- a `master_detail` that is `required` + `readonly`
- a `master_detail` that is `required` + `system`
- a `required` `lookup`, when the object declares no `master_detail`

The full matrix is on the protocol page:
[`MISSING_REQUIRED_FIELD`](/docs/protocol/kernel/error-handling#missing_required_field).

An update or delete **by id** whose *stored* master reference is null answers the same
`422`, whatever the declaration — the caller sent no such field, so nothing could be named
in `fields` and no payload would fix it.

The refusal itself is correct and is not going to be relaxed: on those four shapes
required-field validation does not fire (it skips `system` and `readonly` fields before its
required check, and never fires on a field that is not `required`), so the gate is the only
thing standing between the request and a detail record with a null master reference — which
the `controlled_by_parent` read filter (`fk IN (readable masters)`) can never match, leaving
a record readable by nobody.

These shapes are **authorable today**: lint reports a `master_detail` without `required` as
a warning only, and does not report the `readonly`, `system`, or fallback-`lookup` shapes at
all. **Branch on `code`, treat `fields` as optional, and read the status off the response**
rather than deriving it from the table at the end of this page.
</Callout>

### `INVALID_FORMAT`
**Cause:** Field value does not match the expected format (e.g., invalid email, wrong date format).
**Fix:** Ensure the value matches the field's `format` constraint or built-in type validation.
Expand DownExpand Up@@ -632,6 +671,7 @@ async function handleApiCall() {
| 403 | `authorization` | `PERMISSION_DENIED`, `FIELD_NOT_ACCESSIBLE`, `LICENSE_REQUIRED` |
| 404 | `not_found` | `RECORD_NOT_FOUND`, `OBJECT_NOT_FOUND`, `ENDPOINT_NOT_FOUND` |
| 409 | `conflict` | `CONCURRENT_MODIFICATION`, `DUPLICATE_RECORD`, `DELETE_RESTRICTED` |
| 422 | `validation` | `MISSING_REQUIRED_FIELD` on an absent `controlled_by_parent` master reference (see [above](#missing_required_field)) — this row is an exception to the 400 row, not a second home for the code |
| 429 | `rate_limit` | `RATE_LIMIT_EXCEEDED`, `QUOTA_EXCEEDED` |
| 500 | `server` | `INTERNAL_ERROR`, `DATABASE_ERROR`, `TIMEOUT` |
| 502 | `external` | `EXTERNAL_SERVICE_ERROR`, `INTEGRATION_ERROR` |
Expand Down
43 changes: 41 additions & 2 deletions content/docs/protocol/kernel/error-handling.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -90,7 +90,7 @@ ObjectStack uses standard HTTP status codes:
| **403** | Forbidden | Authenticated but insufficient permissions |
| **404** | Not Found | Resource doesn't exist |
| **409** | Conflict | Resource already exists or version mismatch |
| **422** | Unprocessable Entity | Semantic validation failed (e.g. metadata spec validation) |
| **422** | Unprocessable Entity | Semantic validation failed (e.g. metadata spec validation); also an absent `controlled_by_parent` master reference — see [`MISSING_REQUIRED_FIELD`](#missing_required_field) |
| **429** | Too Many Requests | Rate limit exceeded |
| **500** | Internal Server Error | Server-side error |
| **503** | Service Unavailable | Server overloaded or maintenance |
Expand DownExpand Up@@ -244,7 +244,7 @@ if (error.code === 'VALIDATION_ERROR') {
```

#### `MISSING_REQUIRED_FIELD`
**HTTP Status:** 400
**HTTP Status:** 400 — with one documented exception, which answers **422** (see below)
**Meaning:** Required field is missing

**Example:**
Expand All@@ -262,6 +262,45 @@ if (error.code === 'VALIDATION_ERROR') {
}
```

**Exception — an absent `controlled_by_parent` master reference answers 422 with no `fields`.**
An object whose `sharingModel` is `controlled_by_parent` derives its access from a master
record, so the gate that authorizes writes to it must resolve that master *before* the
executor runs — and the executor is where required-field validation lives. When the master
reference is absent, whichever of the two refuses first decides the envelope, and that
depends on how the reference is declared:

| Master reference declared as | Refused by | Status | `code` | `fields` |
|---|---|:---:|---|:---:|
| `master_detail` + `required`, not `readonly`/`system` | required-field validation | 400 | `VALIDATION_FAILED` | yes |
| `master_detail` with no `required` | the master-access gate | **422** | `MISSING_REQUIRED_FIELD` | **absent** |
| `master_detail` + `required` + `readonly` | the master-access gate | **422** | `MISSING_REQUIRED_FIELD` | **absent** |
| `master_detail` + `required` + `system` | the master-access gate | **422** | `MISSING_REQUIRED_FIELD` | **absent** |
| a `required` `lookup`, when the object declares no `master_detail` | the master-access gate | **422** | `MISSING_REQUIRED_FIELD` | **absent** |

Only the first row is the documented 400 shape. On the other four, messages are prefixed
`[Security] Missing master reference:` and name the object and the field, but no `fields`
array rides along.

The same `422 MISSING_REQUIRED_FIELD` also answers an update or delete **by id** whose
*stored* master reference is null, whatever the declaration. That is a different path: the
caller supplied no such field, so there is no request field to name in `fields` and no
payload that would fix it.

**Why the gate refuses rather than handing over.** Required-field validation skips
provenance-flagged fields before its required check is reached (`system` and `readonly`
fields are skipped outright) and never fires on a field that is not `required` at all. On
those four shapes the gate is the only thing refusing the write, and letting it through was
measured to create a detail record whose master reference is null — a record the
`controlled_by_parent` read filter (`fk IN (readable masters)`) can never match, so it is
readable by nobody and answers 422 on every later write by id. The refusal is correct; only
its status departs from the rule above.

**These shapes are authorable today.** Publish-time lint reports a `master_detail` without
`required` as a *warning* (`relationship/master-detail-required`), and does not report the
`readonly`, `system`, or fallback-`lookup` shapes at all — so a stack can publish clean and
still reach the 422. Branch on `code`, and read the status off the response rather than
deriving it from this page.

#### `INVALID_FIELD`
**HTTP Status:** 400
**Meaning:** Field value has wrong type
Expand Down
Loading