Skip to content

POST /api/v1/automation answers 500 INTERNAL_ERROR for a malformed flow body — including the #4277 undeclared-config-key refusal the #7545 ruling leans on #8055

Closed

Description

@baozhoutao

Same error-class mismatch #7535 just fixed on the sibling POST /automation/:name/toggle route.

Symptom

Four distinct malformed registration bodies, all answered 500 INTERNAL_ERROR where 400 is the right class. Reproduced 2×:

  1. A node missing label → 500 carrying a raw zod issue array: {expected:'string', code:'invalid_type', path:['nodes',0,'label']}
  2. An unknown node key → 500 {code:'unrecognized_keys', keys:['next'], path:['nodes',0]}
  3. A malformed try_catch region → 500 "try_catch 'g' try: invalid region — Invalid input: expected object, received array"
  4. The important one — an undeclared node config key → 500:

    Flow 'x' rejected: 1 undeclared config key(s) (#4277). - node 'n' (notify): unknown config key \totallyBogusKey` at config.totallyBogusKey. It is not declared by this node type's configSchema…`

Expected 400 VALIDATION_FAILED with an ADR-0114 details.fields[], as the sibling /toggle route now returns and as the descriptor routes return.

Contrast controls run in the same pass: a well-formed body still registers 200, and the unknown-flow toggle correctly answers 404 RESOURCE_NOT_FOUND. So the catch arm on this one handler is mapping caller-input refusals to INTERNAL_ERROR.

The substance is fine — only the class and the message shape are wrong

The #7545 premise still holds: the refusal is hard and is located. Case 4 in particular produces exactly the self-correcting message #4277 designed. It is delivered under an HTTP status that says the opposite.

Impact — the one #7535 spelled out

An authoring client with retry-on-5xx will retry a request that can never succeed. And an agent authoring a flow reads "the server broke" instead of "your metadata is wrong" — the inverse of the self-correcting shape #4277 chose. Case 1 additionally leaks a raw zod issue array where the house envelope belongs.

Severity

P3.

Source

Found by the platform checklist retest of automation.flow-node-type-matrix (framework 279ee48a), while verifying the #7545 ruling. Held no clause of that item.

Activity

  1. added
    pm:queueReady for the PM dispatch loop
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    and removed on Aug 12, 2026
  2. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor

    Triage: lands in packages/runtime/src/domains/automation.ts (the registration handler's catch arm, per the route map at :244 and registerFlow at :370-386) → domain:cli, queued. Re-laned from domain:automation — that label is not in the lane table; per the anchoring rule the label is the domain of the package the fix lands in, and this is packages/runtime. Type intent: Bug (native type field not set — list_issue_types still answers 403 for this Routine's credential).

    Serial constraint for the executing seat: #8054 touches the same file (domains/automation.ts:737 plus query-param.ts) — same-file hard serial, never the same batch.

    Size/model suggestion: S–M, opus — same error-class-mapping shape as the sibling /toggle fix this card cites.


    Generated by Claude Code

  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 12, 2026
  4. self-assigned this
    on Aug 12, 2026
  5. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor

    Claimed and dispatched — domain:cli PM seat (#6024), session session_01B3Kurx8qufrDzNjk4rag7V, branch claude/issue-8055-automation-register-error-class.

    Honoring the triage seat's serial constraint: #8054 and #7968 also land in packages/runtime/src/domains/automation.ts and are held out of this batch. #8055 goes first as the longer pole; the other two are queued behind it and will be dispatched once this lands.

    Dispatch shape: mirror the sibling /toggle fix (#7535) — the catch arm must classify caller-input refusals as 400 VALIDATION_FAILED with an ADR-0114 details.fields[], not 500 INTERNAL_ERROR. Case 4 (the #4277 undeclared-config-key refusal) is the one that matters most: it already produces exactly the self-correcting message #4277 designed, delivered under a status that says the opposite. Case 1's raw zod issue array must stop reaching the wire — the house envelope belongs there.

    ⛔ Not licensed by this card: widening the refusal, softening any of the four refusals into a 200, or changing which bodies are rejected. Only the class and the envelope shape move. A contrast control that a well-formed body still registers 200 is part of the deliverable, not an optional extra.


    Generated by Claude Code

  6. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor
    {
      "issue": 8055,
      "status": "done",
      "branch": "claude/issue-8055-automation-register-error-class",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/8125",
      "premise_still_valid": true,
      "summary": "The premise was verified against origin/main and holds exactly as filed: AutomationEngine.registerFlow raises every refusal (FlowSchema.parse, validateControlFlow, validateNodeConfigKeys #4277, validateFlowExpressions) as a plain throw with no .status, the POST / branch in packages/runtime/src/domains/automation.ts did not catch, and dispatch() re-throws non-denials — so the transport's outer catch answered 500 and copied any .issues into details verbatim. The branch now wraps its registerFlow call and converts the engine's verdict through a new module-local flowDefinitionRefusal: 400 VALIDATION_FAILED with an ADR-0114 details.fields[], the raw Zod issue array mapped to fields[] instead of forwarded, and the #4277 self-correcting message preserved verbatim (a 400 never reaches the 5xx sanitiser). An error that declares its own .status/.statusCode still keeps it, and which bodies are refused is unchanged. IMPORTANT DEVIATION FROM THE DISPATCH MODEL, deliberate and argued in the PR: #7535's fix (PR #7558) does NOT transfer as a mechanism — it fixed a 404 not-found with an existence probe BEFORE the service call, over a closed one-condition refusal set. This route's refusals are open (arbitrary Zod parse failures plus two descriptor-driven engine checks) and none is decidable without calling registerFlow, so a pre-check would mean re-implementing the engine's validation in the transport. What carries over, and is why the two routes converge rather than growing a third spelling, is #7558's stated rule that the HTTP class of a plain domain error is the serving boundary's decision, made at the domain handler and not in a generic catch.",
      "tests": "NEW packages/runtime/src/domains/automation-register-error-class.test.ts (9 tests). Cases 1-3 are produced by PRODUCTION code — the fake's registerFlow calls the real FlowSchema.parse and validateControlFlow from @objectstack/spec/automation, the same calls canonicalizeStoredFlow makes, in the same order; the captured shapes are byte-identical to the issue's ({expected:'string',code:'invalid_type',path:['nodes',0,'label']}; {code:'unrecognized_keys',keys:['next'],path:['nodes',0]}; \"try_catch 'g' try: invalid region — Invalid input: expected object, received array\"). Only #4277 is modelled, from engine.ts's own construction, because that check is descriptor-driven and lives in a package runtime does not depend on. ANTI-VACUITY: no assertion is status!==500 or status>=400; each case asserts status AND error.code AND error.httpStatus AND the presence and per-entry shape of details.fields[], plus its substance — case 1 pins Object.keys(details)===['fields'], details.issues gone, per-entry keys exactly field/code/message, no Zod-internal KEY (\"expected\"/\"received\"/\"path\") anywhere in the serialised body, and nodes.0.label surviving; case 4 pins the #4277 text clause by clause (#4277, unknown config key `totallyBogusKey`, at config.totallyBogusKey, \"not declared by this node type's configSchema\", the \"Declared here: ...\" prescription, node 'n' (notify)) and that the same text reaches the field entry. CONTRAST CONTROLS, green both directions: well-formed body still 200 and still reaches the service; #3899 nameless-definition check still refuses before registerFlow; unknown-flow toggle still 404 RESOURCE_NOT_FOUND. REVERSE VERIFICATION (git checkout, never git stash; restored from a patch file; restore confirmed byte-identical by sha256sum -c with git diff --stat unchanged) — 6 of 9 red, 3 green, and the 3 green are the contrast controls, which is the correct direction for them. Verbatim: ' × case 1 — a node missing `label`: the raw Zod issue array never reaches the wire / ZodError: [' ; ' × case 2 — an unknown node key: located, named, and free of Zod internals / ZodError: [' ; \" × case 3 — a malformed `try_catch` region: the engine's own sentence survives / Error: try_catch 'g' try: invalid region — Invalid input: expected object, received array\" ; \" × case 4 — the #4277 self-correcting text arrives INTACT under the honest status / Error: Flow 'welcome_flow' rejected: 1 undeclared config key(s) (#4277).\" ; ' × no malformed body reaches 500 by any route — no retry-on-5xx client is provoked / ZodError: [' ; ' × an engine error that DECLARES its own class keeps it — the boundary supplies a default, not a verdict / Error: flow store unreachable' ; 'Tests  6 failed | 3 passed (9)'. HONEST NOTE, not dressed to fit the template: the reversal red is 'the promise rejects', not 'the status was 500' — that is the truthful pre-fix behaviour AT THIS SEAM (the handler produced no response at all; the 500 is what the transport's outer catch makes of the escape). The general rule for that escape is already pinned by dispatcher-validation-error.test.ts (#3918, 'leaves a non-validation error on its old path'), so it is cited rather than duplicated. GATES: pnpm --filter @objectstack/runtime test = 143 files / 2185 tests all passing; pnpm --filter @objectstack/runtime typecheck = clean; pnpm check:type-check-debt = OK, runtime TEST_DEBT ceiling 227 held EXACTLY (not raised, and runtime is not among the entries the gate reports as lowerable — the new test file added zero hidden-layer errors); it required a full closure build first (pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*'), which the gate refuses to run without; node scripts/check-nul-bytes.mjs = OK plus a control-character self-scan of every changed file; eslint --no-inline-config on both changed files = clean. CI on PR #8125 was still in_progress at report time (per the draft-PR-time reporting contract).",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #8123: PUT /api/v1/automation/:name makes the identical registerFlow call in the same file and still answers 500 for all four bodies — after this PR merges, POST and PUT disagree about the class of an identical refusal until #8123 lands. Not fixed here because #8055's scope is the POST route; the helper added is module-local and route-agnostic so that fix is one line. Carries a Blocked-by: #8055 line.",
        "filed as #8124: packages/runtime/src/validation-failure.ts's fieldsFromZodIssues passes Zod's issue codes straight through to fields[].code, which ADR-0114 D2/D3 closed (FieldErrorCode, 27 members; 'unrecognized_keys' is not one). Affects /analytics, /notifications and this route — the pass-through is the package's, not any route's, and the compliant mapper (zodIssuesToFields) is module-local inside @objectstack/rest's rest-server.ts. Filed as one card with three fix options rather than forked locally, which would make automation a third dialect inside one package."
      ]
    }

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they landpm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions