Skip to content

A missing required master-detail parent still answers 422 MISSING_REQUIRED_FIELD with no fields[] and a [Security] message — while the same field, present-but-unresolvable, answers 400 VALIDATION_FAILED with fields[] (#7474 residual, 17.0.0 GA) #8688

Description

@hotlong

Part of objectstack-ai/hotcrm#691. Residual of #7474 ("assertControlledByParentWrite answers a metadata defect and a missing row with the same 403 PERMISSION_DENIED", closed). Measured live on @objectstack/* 17.0.0 GA over REST, one server, one session.

Fixed by #7474, recorded so it is not re-litigated

The response is no longer 403 PERMISSION_DENIED. A UI mapping 403 to "you lack permission" no longer misfires. That was the reporting card's headline harm and it is gone.

Still live

POST /api/v1/data/crm_contact {"first_name":"Probe","last_name":"NoMaster","email":"…"}
  -> HTTP 422
     code   : MISSING_REQUIRED_FIELD
     error  : [Security] Missing master reference: insert on 'crm_contact' did not supply
              'crm_account'. A controlled_by_parent detail derives its access from its
              master, so 'crm_account' must carry a master record id on every write.
     fields : (absent)

Measured against every adjacent branch on the same server:

case response fields[]
required master-detail parent omitted 422 MISSING_REQUIRED_FIELD absent
required master-detail parent explicitly null 422 MISSING_REQUIRED_FIELD absent
required master-detail parent present but unresolvable 400 VALIDATION_FAILED present — code: reference_not_found
ordinary required lookup + scalars omitted (other object) 400 VALIDATION_FAILED present, 3 entries
ordinary required scalar omitted, same object, master supplied 400 VALIDATION_FAILED present, 2 entries
undeclared field 400 INVALID_FIELD absent

Three things follow:

  1. The status and code are still the odd ones out. Every other missing-required-field answer is 400 VALIDATION_FAILED.
  2. There is still no fields[], so a form cannot highlight the offending input — while the adjacent failure on the very same field does supply it.
  3. The message still opens [Security], so any surface rendering the message string still shows a missing required field as a security refusal.

The ordering is unchanged, and it is measurable

With both the master and an ordinary required scalar omitted, the answer is the guard's, not validation's:

{"first_name":"Probe"}                              -> 422 MISSING_REQUIRED_FIELD (master guard)
{"first_name":"Probe","crm_account":"<real id>"}    -> 400 VALIDATION_FAILED, fields:[last_name, email]

So assertControlledByParentWrite still runs ahead of required-field validation and short-circuits it. #7474 changed the guard's verdict wording; it did not move the guard.

Why the fix is small

The platform already emits the desired envelope for this exact field, one case over:

{"error":"Account: no crm_account record has id \"no_such_account_1153\"",
 "code":"VALIDATION_FAILED",
 "fields":[{"field":"crm_account","code":"reference_not_found","label":"Account",
            "constraint":{"target":"crm_account"},"value":"no_such_account_1153"}]}

An absent required master and a wrong one are answered by two different subsystems in two different envelopes. Letting required-field validation report the absent case first — and reserving the access guard for a master that is present but not writable by the caller, which is genuinely a permission outcome and is already correct today — collapses that split without inventing a new response shape.

Repro

Any object with Field.masterDetail(parent, { required: true }) under sharingModel: 'controlled_by_parent'; POST it without the parent, then with an unresolvable parent, then with a real parent and a missing scalar; compare status, code and fields[].


Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions