Repository navigation
fix(rest): a hook's onError abort is no longer logged as an unhandled error - #27690
Conversation
…ndled error A hook that aborts its write under onError: 'abort' with a business throw is a refusal the route already answered (the code-less 400). logUnexpectedRouteError now recognises it by the sandbox marker every door reads (sandboxBusinessMessage: innerMessage set, not a crash) and the code-less 4xx answer, and no longer prints '[REST] Unhandled error' for it. The wire answer is unchanged. Claude-Session: https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F Co-authored-by: Claude <noreply@anthropic.com>
…wire answer unchanged Claude-Session: https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F Co-authored-by: Claude <noreply@anthropic.com>
…with no unhandled-error line Claude-Session: https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F Co-authored-by: Claude <noreply@anthropic.com>
…ok-abort-not-unhandled
…ot the answer A code the door adds to an undeclared abort's answer is the door's, not the body's, so it must not turn the refusal back into a logged fault. Claude-Session: https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 19 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a9c0c1355d2ce0fb06156a0f5ab32d2aed0ab680 && git checkout a9c0c1355d2ce0fb06156a0f5ab32d2aed0ab680
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin da7bcc8a89c68c7d6579a3ca6ed11157e815e244 2e4954fd5331b6776346c421ad07d7835ca5303e && git checkout -B drift-repro da7bcc8a89c68c7d6579a3ca6ed11157e815e244 && git merge --no-ff 2e4954fd5331b6776346c421ad07d7835ca5303e
node scripts/docs-audit/affected-docs.mjs --json da7bcc8a89c68c7d6579a3ca6ed11157e815e244
|
|
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F 维护者速读(席位终稿)
席位意见: ACCEPT。只动日志不动线上答复;「要不要给这种拒绝一个登记码」是 spec 车道的另一半(#27597 / PR #27667),本 PR 的判断读抛出的错误而不是答复,那一半落地后也不会重新变吵。 席位核对ACCEPT · head
|
Conflict: packages/rest/src/rest-declared-client-refusal-log.test.ts. Main (#27690) removed the loud "un-coded business-rule 400" case and added the #27508 quiet block. Took main's side, and flipped that block's two wire pins to carry the door-added HOOK_ABORTED. The log verdict reads the thrown error, so it stays quiet. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufziZEEbJZ3DUMwdVJWX
Requested by Sales · project thread
Refs #27508 (log half)
Clause-②: no
Metadata-protocol: none
Skills-docs: none
维护者速读
onError: 'abort'(默认)抛出业务错误中止写入时,服务端不再把它记成[REST] Unhandled error;线上应答(400、正文、信封)一个字节都不变。packages/rest/src/error-response.ts的isSandboxBodyRefusal与logUnexpectedRouteError调用处;packages/rest/src/rest-declared-client-refusal-log.test.ts新增的 describe 块。pnpm --filter @objectstack/rest exec vitest run src/rest-declared-client-refusal-log.test.ts,预期 16 条全绿(其中 hook 中止两扇门都无 Unhandled 行,三条对照照旧记录)。What changes
packages/rest/src/error-response.ts: a new predicate,isSandboxBodyRefusal, joins the three readslogUnexpectedRouteErroralready makes. It is true when all three hold:sandboxBusinessMessage:innerMessageset, and not a native-error crash); andresolveThrownHttpError(...).declaredCodeis absent). This is read off the thrown error, not the answer, so a code the door itself adds to an undeclared abort later does not make the abort loud again.resolveErrorResponse,mapDataErrorandpackages/typesare untouched. The ADR-0112 ledger is not touched and no code is minted; that half belongs to the spec lane.info([BodyRunner] sandboxed hook refused). The error-level line removed here was the second line for one ordinary refusal.isExpectedRouteErrorandisDeclaredClientRefusalthat said the sandbox unwrap's un-coded 400 "is still logged" now point at the new read.@objectstack/restpatch.Tests
packages/rest/src/rest-declared-client-refusal-log.test.ts. The standing control "an un-coded business-rule 400 (a sandboxed hook refusal) logs" pinned the old verdict, so it moves into a new block that pins what triage ruled. It goes through the real route handlers (POST /api/v1/data/:objectthroughmapDataError+logUnexpectedRouteError, andPUT /api/v1/meta/:type/:namethroughhandleRouteError):{ error: 'boom abort', object: 'qa_media' }at 400 on the data door and{ error: 'boom abort' }at 400 on the meta door, bothtoEqual, and logs no[REST] Unhandled errorline;TypeErrorat the same door is a 500 and is logged, checked by identity;packages/qa/dogfood/test/object-hook-lifecycle-scratch.dogfood.test.tsacceptance[6] is the real door with a real QuickJSSandboxError: abeforeInserthook declaredonError: 'abort'throws. It now also asserts the exact body{ error, object }and that the request logs no[REST] Unhandled errorline.Ablation 1: unit route pin (remove the call-site exemption, then restore)
scripts/ablation-replace.mjs, with the anchor on the&& !isSandboxBodyRefusal(error, resolved)call-site line, at head2e4954fd:Both tests fail only at the log assertion. Their status and exact-body assertions come first and pass under the ablation, which is the pre-change logic. So the wire answer is the same as before.
Ablation 2: real-door dogfood pin (mutate, rebuild rest dist, preflight, run, restore, rebuild, preflight)
This ran at
17713e7b. The later commit2e4954fdchanges only condition 3 of the predicate, not the call-site anchor; ablation 1 was re-run on it.Acceptance notes
SandboxErrorcarries no body kind, so a sandboxed action's code-less business refusal answered at a data door is quiet too. It is the same shape and the same answer. A message-prefix read (hook '…' threw:) would tell the two apart, but that would be a text heuristic, so it was not used.HOOK_ABORTEDand "still logs"), so a text conflict inrest-declared-client-refusal-log.test.tsis expected for whichever PR lands second. The predicate reads the thrown error's code, not the answer's, so a door-assignedHOOK_ABORTEDkeeps the abort quiet. When that PR rebases, its "still logs" line should become "quiet".undeclaredHookRefusalAtRun) becomes aSandboxErrorwith noinnerMessage. That is the sandbox's own fault (500), so it stays loud, unchanged.Verification
All local readings below are at head
2e4954fdunless noted.Package tests, typecheck, lint
pnpm --filter @objectstack/rest test:Test Files 336 passed (336),Tests 6602 passed | 326 skipped (6928), exit 0.pnpm --filter @objectstack/rest typecheck: exit 0.pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/object-hook-lifecycle-scratch.dogfood.test.ts:Tests 4 passed (4), exit 0.--no-inline-config) onpackages/restand the dogfood test file: exit 0.Derived gate families
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 64 families at the final change set. Each was run with its exit code captured before any pipe, and all 64 exited 0. The run was split across a container restart: the families not yet run were re-run at the same head,2e4954fd.check:type-check-debtwas run whole under the verify lock after a queue timeout gave it PREREQUISITE NOT MET (exit 3). Its re-run exited 0.check:dual-build-cjs-loadsexited 0 once a full build had run, after an earlier PREREQUISITE NOT MET (exit 3).Reconciliation,
--ran:The full dogfood battery is not derived: there is no
!in the title and no(narrowing)arm. The repo-widepnpm lintand the rest of the farm are left to CI.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F