Skip to content

REST: clone and createMany report a readonly strip in the body's droppedFields but send no X-ObjectStack-Dropped-Fields header, unlike single create and update #28933

Closed

Description

@objectstack-fleet

domain:devx#1 · os-warren · session_01JQwxbZpHPpovxXPremPLtv

立卡门: ① 产品缺陷,具名落点 + 复现(reach: 公开入口 HTTP clone 与 createMany 实测一次错误结果)
业务理由: 依赖 X-ObjectStack-Dropped-Fields 警告头(#3431)发现「字段没写进去」的 API 集成方,在批量创建和克隆这两条门上收不到它,只读响应头的客户端会把被剥掉的只读字段当作已写入。不修,同一个契约在不同写门上说法不一。
动手的读者: domain:server 席,packages/rest/src/crud-routes.ts 的 clone 与 createMany 处理器调用 applyDroppedFieldsHeader,附 HTTP pin。
查重: repo is:issue X-ObjectStack-Dropped-Fields 430 命中、droppedFields header clone 31、droppedFields createMany header 19(均含已关);最近邻 #15703(clone 正文缺 droppedFields,已关,已修正文这一面)、#15777 都不是响应头。

QA-source: #28927 · access-security.write-path-guards · observed beside the clauses

What happens

X-ObjectStack-Dropped-Fields (#3431) surfaces a legal write strip on "the REST write response". Single POST /data/:object and PATCH /data/:object/:id send it. Two other write doors that strip the same fields send only the body channel:

  • POST /data/:object/:id/clone → 201 with droppedFields:[{object, fields:[lead_score], reason:readonly}] in the body, no header;
  • a bare-array createMany POST /data/:object with […] → 201 with droppedFields in the body, no header.

A caller that reads the header (the documented "warning header" channel) misses these strips.

Measured at framework 7fa9873f (main, 2026-11-02) on a stock showcase boot, seen independently by two runners.

Reproduction

  1. Boot the stock showcase.
  2. As admin: POST /api/v1/data/showcase_contact {"name":"h1","lead_score":10} → 201 with header X-ObjectStack-Dropped-Fields: lead_score;reason=readonly (control).
  3. As admin: POST /api/v1/data/showcase_contact [{"name":"h2","lead_score":10},{"name":"h3","lead_score":10}] → 201; body droppedFields names lead_score; no header.
  4. As admin: POST /api/v1/data/showcase_contact/ID/clone {"overrides":{"lead_score":42}} → 201; body droppedFields names lead_score; no header.
  5. Expected: the header on every 2xx write answer whose body carries droppedFields.

Where

applyDroppedFieldsHeader (packages/rest/src/error-response.ts) is called only from the single create and update handlers in packages/rest/src/crud-routes.ts (around L304 and L495); the clone and createMany handlers do not call it.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: access-security.write-path-guards

    Triage: priority:p2 · domain:server · pm:queue · 2026-11-02T05:05Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. added
    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobs
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    on Nov 2, 2026
  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

    Claim: PM loop round 4 · card thread · 2026-11-02T05:21Z
    Session: session_0186vfjHHMsRXC9EUT9hTFMo (the seat); the card thread is session_01FEkQ5nE2GswUTcEX8HbTmt
    Account: os-project-manager (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-gibefk (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: the card thread's own container checkout
    Domain: domain:server
    Seat: domain:server#2
    Ruling-ref: the maintainer, 2026-11-01T03:46Z: 「server 车道现在就是你负责,任务有一大堆啊,都可以派,包括 p3」, with 2026-10-26T10:29Z 「p1 p2 处理完了,就可以处理 p3」. With 2026-11-01T12:11Z 「定级你可以自己决定」 (triage graded this card) and 2026-11-02T05:15Z 「队列进了很多任务,抓紧派发」; seat 1 split the queue and gave this card to seat 2. Seat's route: every 2xx REST write answer whose body carries droppedFields sends the header through the existing applyDroppedFieldsHeader; createMany gets the union of the rows' stripped fields.
    File surface: packages/rest/src/crud-routes.ts write handlers (clone, createMany, and any other write door measured to lack the header); their tests; one dogfood pin; changeset
    Container & model: M, mode:thread, model: opus (--tier names no path mandate: PM's call, default), effort: medium
    Clause-②: yes (widening) expected: a header added to answers; the dev measures and declares
    Metadata-protocol: none (expected; the dev measures and declares)
    Responsibility: the dev routes the strip report through the one header seam, red first with ablation; the seat reviews and lands
    Thread-read: none
    Premise: measured by triage (6569529210) on main
    Serial constraints cleared: none; overlap re-read of open PRs on crud-routes.ts before pr_create
    Resume-from: none — fresh from origin/main

    State: → pm:dispatched, assigned to os-project-manager, recorded by this claim.


    Generated by Claude Code

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 2, 2026
  5. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-project-manager · session_01FEkQ5nE2GswUTcEX8HbTmt
    os-dev-report
    {
    "issue": 28933,
    "status": "done",
    "branch": "claude/project-thread-gibefk",
    "pr": "#29034",
    "session": "session_01FEkQ5nE2GswUTcEX8HbTmt",
    "premise_still_valid": true,
    "summary": "Every 2xx REST write answer whose body carries droppedFields now sends X-ObjectStack-Dropped-Fields through the one seam applyDroppedFieldsHeader: clone, createMany (header = the body's union), per-object batch, updateMany and import commit (header = union of results[i].droppedFields), and the cross-object /batch (union of its per-op events). The seam takes an optional rows argument and emits each field;reason token once; single create/update values are unchanged. The stale roll-up 201 of clone and createMany now goes through earnedWriteDespiteStaleSummary so it carries the header too; an import dry run writes nothing and sends none. Spec describe text (clone, createMany, batch row), the client CloneDataResult docblock and data-api.mdx stopped saying no header; reference docs regenerated. Premise note: the card's bare-array createMany path POST /data/:object refuses arrays (400, #3899); the real door is POST /data/:object/createMany. File surface is wider than the claim's (crud-routes.ts is untouched): batch-routes.ts, data-action-routes.ts, error-response.ts, stale-summary-answer.ts, rest-server.ts (comment), spec batch.zod.ts and protocol.zod.ts (describe text only), client index.ts (docblock), content/docs/api/data-api.mdx, content/docs/references/api/batch.mdx and protocol.mdx (regenerated). The spec touch means a contract review is owed before landing.",
    "tests": "At a6b0d83: new packages/rest/src/rest-dropped-fields-every-write-door.test.ts (15 cases, real ObjectQL + protocol on better-sqlite3); on main 135054e it went 11 failed / 4 passed (controls), on the fix 15/15. Header suites together (7 files): Tests 66 passed (66). pnpm --filter @objectstack/rest test: Test Files 367 passed (367), Tests 6951 passed, 332 skipped, VERDICT command-exit 0. rest + client typecheck (rest includes check:test-typecheck): VERDICT command-exit 0. Dogfood showcase-static-readonly (4 cases added): on main the clone header case red (null) and createMany cases red; on the fix Tests 8 passed (8). Ablation via scripts/ablation-replace.mjs (anchor hits 1, restore blob == HEAD each leg), each against the 15-case pin: clone success -> 1 red (clone overrides); clone stale answer -> 1 (clone earned); createMany success -> 3 (createMany strip cases); createMany stale answer -> 1; per-object batch -> 1; updateMany -> 1; import commit -> 1; import dry-run guard -> 1 (import case); cross-object -> 1; seam token dedupe -> 4 (updateMany, batch, import, cross-object); seam per-row reading -> 3 (updateMany, batch, import). Dist-resolved dogfood leg: clone + createMany calls replaced by markers, rest rebuilt, ablation-dist-preflight found both markers in dist, dogfood Tests 3 failed | 5 passed; restore leg rebuilt, preflight --absent exit 0 for both, tree clean, Tests 8 passed.",
    "gates": "Local: dispatch-gates --commands derived 106; all 106 recorded :: exit 0 at a6b0d83; dispatch-gates --ran: 106 run, 0 NOT-MEASURED, 0 UNRUN (exit 0). First pass had 8 x exit 3 (PREREQUISITE NOT MET: spec dist predated the describe edit; check-plugin-teardown-shape --self-test needed git fetch --depth=1 of 621a487) and check:generated exit 1 on the same stale spec dist; after pnpm --filter @objectstack/spec build and the fetch all 9 reran exit 0. Full ran list: /mnt/project-files/pm-orders/server/28933-gates-ran.txt. CI check-runs on #29034: in_progress at report time (not awaited, per os-dev).",
    "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm; the one touched dogfood file was run: Tests 8 passed (8)",
    "mcp_calls": "6 — issue_read x3 (28933 get, get_comments x2), list_pull_requests x1, pull_request_read get_files x1 (28731, overlap), plus this thread's hearthbot messaging; no GitHub write tool",
    "api_writes": "2 — fleet-write pr_create (#29034, run 41399043192); this os-dev-report comment on #28933 via post-stamped. git push is not a REST write.",
    "open_questions": [
    {
    "question": "Import dry run: send the header or not? It previews the strip in results[i].droppedFields but writes nothing.",
    "options": [
    "A: no header on a dry run (shipped): the header says a write's fields never landed",
    "B: send it on the dry run too, mirroring the body"
    ],
    "recommendation": "A, because the ruling names 2xx WRITE answers and a dry run is not one; flipping is one line (drop the dryRun guard), pinned by the import case."
    }
    ],
    "out_of_scope_findings": [
    "class: a · reach: POST /api/v1/data/:object/:id/clone measured on main 135054e in a real-stack rest test (a clone of a row whose readonly column ro2 is null answers droppedFields naming ro2) and on the stock showcase (a plain clone of a contact with null lead_score answers droppedFields naming lead_score); the console announces every non-empty droppedFields · evidence: cloneData copies the source row (including null readonly columns) and engine.insert's readonly strip reports a present key whatever its value, so a clone reports drops the caller never made, and with this PR the header repeats it · dedupe words: clone droppedFields null readonly, cloneData copies null readonly, clone phantom dropped field",
    "carrier: none · noted, not filed: the runtime dispatcher's POST and PATCH /data arms (packages/runtime/src/domains/data.ts, the @objectstack/hono catch-all twin) set no X-ObjectStack-Dropped-Fields even for single create/update; reach not measured (whether that arm is reachable while RestServer mounts the data routes)"
    ]
    }


    Generated by Claude Code

  6. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

    Release: session_0186vfjHHMsRXC9EUT9hTFMo · cause: the maintainer's wrap-up (「进展中的开发先提交代码然后暂停,先确保已有的pr都合并」, 「草稿的也交给下一任,在座位帖写清楚」); seat 2 signs off · next: PR #29034 is ACCEPTed (6570761103) with review PASS (6570720694) and is in the merge queue; Fixes #28933 closes this card on merge. Next: landing record, and file the clone null-readonly follow-up card (dev report 6570533136). ⛔ Do not re-dispatch while #29034 is queued.
    Resume-from: claude/project-thread-gibefk@a6b0d839d45a4e08037beab8bd4efb60b2fcb369

    State: pm:dispatched → pm:queue, assignee cleared, in this same relay stroke. Seat hand-off: mailbox #28284 comment 6571506250; seat post #20966.


    Generated by Claude Code

  7. added
    pm:queueReady for the PM dispatch loop
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Nov 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions