Repository navigation
fix(rest)!: answer 404 RESOURCE_NOT_FOUND for an unknown resource at the package draft, flow runs and datasource by-name doors - #25628
Conversation
…he draft, runs and datasource by-name doors Claude-Session: https://claude.ai/code/session_01X15AahTqrmeh8gAVdpKMCK Co-authored-by: Claude <noreply@anthropic.com>
…urce by-name doors Claude-Session: https://claude.ai/code/session_01X15AahTqrmeh8gAVdpKMCK Co-authored-by: Claude <noreply@anthropic.com>
…source doors Claude-Session: https://claude.ai/code/session_01X15AahTqrmeh8gAVdpKMCK Co-authored-by: Claude <noreply@anthropic.com>
…known-resource-404
📓 Docs Drift CheckThis PR changes 2 package(s): 21 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 10 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 28 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 0c517c4616ad08325a426cf7cf6c2792d0a701ea && git checkout 0c517c4616ad08325a426cf7cf6c2792d0a701ea
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f4860da151df014ef093c87d6517d78d92b009d9 f3e50da5f457813415390a24b0daa84085e2cb05 && git checkout -B drift-repro f4860da151df014ef093c87d6517d78d92b009d9 && git merge --no-ff f3e50da5f457813415390a24b0daa84085e2cb05
node scripts/docs-audit/affected-docs.mjs --json f4860da151df014ef093c87d6517d78d92b009d9
|
…and GET /packages/:id/commits for an unknown resource Claude-Session: https://claude.ai/code/session_01X15AahTqrmeh8gAVdpKMCK Co-authored-by: Claude <noreply@anthropic.com>
席位核对REWORK ·
Generated by Claude Code |
…th GET, keep a failed flip's unhideError Claude-Session: https://claude.ai/code/session_01X15AahTqrmeh8gAVdpKMCK Co-authored-by: Claude <noreply@anthropic.com>
…gration label Claude-Session: https://claude.ai/code/session_01X15AahTqrmeh8gAVdpKMCK Co-authored-by: Claude <noreply@anthropic.com>
席位核对ACCEPT (seat review,
Not landed in this act. The Generated by Claude Code |
Contract reviewServed-tier: Face: ① Derived judgmentsEight REST doors narrow their served set for a resource that does not exist; each now answers what its read sibling already answered for the same name. Per door:
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS
Generated by Claude Code |
Fixes #25486
Clause-②: no (narrowing)
Metadata-protocol: none
BREAKING (a served-set narrowing of eight REST doors): a request that names a package id, flow name or datasource name that does not exist is now refused
404 RESOURCE_NOT_FOUND. Five of these doors used to answer it200with an empty result, and three answered400 DATASOURCE_ADMIN_ERROR. The changeset shipspatchwith the ADR-0087 dispositionnot-required (no-migration-prescription): no metadata key changes, only the HTTP answer for a resource that is not there.What changes
Eight doors answered a request about a resource that does not exist as if it were empty or malformed, while their sibling doors answered
404for the same name. Each now answers404 RESOURCE_NOT_FOUNDwith its sibling's status, code and message.RESOURCE_NOT_FOUNDis an existing standard code; no new code is registered.POST /api/v1/packages/:id/publish-drafts(unknown id)200 { success: false, outcome: 'nothing_to_publish' }404 "Package 'ID' not found"GET /api/v1/packages/:idPOST /api/v1/packages/:id/discard-drafts(unknown id)200 { success: true, discardedCount: 0 }404GET /api/v1/packages/:idGET /api/v1/packages/:id/commits(unknown id)200 { commits: [] }404GET /api/v1/packages/:idGET /api/v1/automation/:name/runs(unknown flow)200 { runs: [], hasMore: false }404 "Flow 'NAME' not found"POST /api/v1/automation/:name/triggerDELETE /api/v1/automation/:name(unknown flow)200 { name, deleted: true }404POST /api/v1/automation/:name/triggerPATCH /api/v1/datasources/:name(unknown name)400 DATASOURCE_ADMIN_ERROR "Datasource 'NAME' not found."404 'Datasource "NAME" does not exist.'GET /api/v1/datasources/:name, word for wordDELETE /api/v1/datasources/:name(unknown name)400404GET /api/v1/datasources/:namePOST /api/v1/datasources/:name/migrate-credential(unknown name)400404GET /api/v1/datasources/:nameA resource that exists keeps its answer: an empty runs page,
nothing_to_publish, an empty commit trail, and400 DATASOURCE_ADMIN_ERRORfor a code-defined datasource are all unchanged.Where absence is decided, per door
DatasourceAdminService:updateDatasource,removeDatasourceandmigrateCredentialthrew a plainError. They now throwdatasourceNotFoundError(name), which carriescode: 'RESOURCE_NOT_FOUND'andstatus: 404(the same shape as the service's existingmetadataIncompleteError503). Its message isGET's own words,Datasource "NAME" does not exist.. The route's error renderer (badRequestinadmin-routes.ts) relays that envelope, exactly as it already relayed the 503. All four by-name doors ofdatasource-adminare covered;GETwas already right.GET /:name/runsnow asksflowIsUnknown(packages/runtime/src/flow-dispatch-status.ts) before listing. That is the trigger door's own registry probe and message, reused rather than written a second time. It is asked before the query is judged, as the trigger door asks it. A service withoutgetFlowcannot be asked, so it lists as before.200 { name, deleted: true }.unregisterFlowis a no-op for a name the engine does not hold, anddeleteMetaItemanswers a name with no row{ success: true, reset: false }("nothing to delete").unregisterAndDeleteFlownow asksflowIsUnknown. It answers404withflowNotFoundMessageonly when the engine did not hold the flow AND the store deleted no row (reset !== true).qlService.registry.getPackage) is the existence authority every package read sibling answers its 404 from. Authored packages are installed into it too (POST /packages,service-packageat boot). The runtime packages domain decides absence, sopackages/metadata-protocol/src/protocol.tsis untouched, and no [maintainer] refactor(metadata-protocol): splitsrc/protocol.ts(33,065 lines, +10,169 in 30 days) along itsObjectStackProtocolImplementationseams, one move-only slice per PR #25467 region is near this diff. The 404 is answered only when the protocol's answer was empty (nothing_to_publish), nothing was un-hidden, no visibility-flip failure was recorded (unhideError), and the registry lacks the id. Drafts the protocol holds for such an id are still published.packageIsUnknownrule, after the protocol answers and only when that answer is empty. Drafts or commits held for an id the registry lacks are still discarded or listed.discard-draftsis the twin ofpublish-drafts, fixed in place under os-dev rule 3's bounded exemption.commitsand flow delete were added in the seat's patch round, as members of this closing card's family in the files this PR already edits.Consumers (A4)
ObjectStackClient.fetchthrows on any non-2xx answer, using the envelope's message. Sopackages.publishDrafts,packages.discardDrafts,packages.listCommits,automation.listRunsandautomation.deletenow reject loudly for an unknown id; there is no crash path. No in-repo non-test consumer calls these doors for an id it did not first install or register: the dogfood callers allPOST /packagesfirst. objectui consumers: see trade-off (b) below.Accepted trade-offs (recorded; no code change)
200.GET /api/v1/automation/:name/runs/:runIdstill answers by run id alone, as before.404atGET /:name/runs. Before, it got an empty page. objectui'sFlowRunsPanel.tsx(around lines 240–244) renders that rejection as "unavailable".DELETE /:namecounts existence in the store or the engine, while runs, trigger andGET /:namecount the engine only. That asymmetry is what keeps a flow that failed to register deletable at this door.Pins (whole-repo sweep)
datasource-admin-service.test.ts. The three "missing datasource" cases assertedtoThrow(/not found/). They now assert{ code: 'RESOURCE_NOT_FOUND', status: 404, message: 'Datasource "nope" does not exist.' }.envelope.conformance.test.ts: the "credential-migration failure" row used a not-found message for its400case. It now uses a store failure, because an unknown name no longer takes that arm.statuspluserror.codeplus the named resource, and adds a side-by-side parity case against the sibling door:service-datasource/src/__tests__/datasource-unknown-name.test.ts: the real service behind the real routes. All four by-name doors give one answer: status, code and message. Nothing is written. Controls: a known datasource patches, and a code-defined one is still400.runtime/src/domains/automation-runs-unknown-flow.test.ts:404with the store never asked. Parity withPOST /:name/trigger. Existence is judged before the query. Control: an existing flow with no runs still gets200 [].runtime/src/domains/automation-delete-unknown-flow.test.ts:404naming the flow. Parity with the trigger door. Controls: an engine-held flow and a stored-only flow are both still deleted,200.runtime/src/domains/packages-draft-doors-unknown-package.test.ts:publish-drafts,discard-draftsandcommitsall answer404, each in parity withGET /packages/:id, which is itself asserted404. Controls: a known empty package gets200at all three doors. Drafts held for an unregistered id still publish and still discard. Commits held for an unregistered id are still listed. An un-hidden app is reported, not answered404. A failed visibility flip answers200with itsunhideError.docs/qa/platform-checklist/areas/*.json(integration-system.datasource-admin-lifecyclealready states "unknown name → 404 RESOURCE_NOT_FOUND"),packages/qa/dogfood/test/**,packages/cli/test/**,packages/client/src/**.Verification record
Final head
f3e50da5f, on base merge7da7e5a59(fromorigin/main481ac6211).f3e50da5f, run underos-verify-lockwith CI's commandpnpm --filter @objectstack/dogfood test(no shard, no path):VERDICT command-exit 0. Test Files: 293 passed, 3 skipped (296). Tests: 2509 passed, 18 skipped (2527). No failure.f3e50da5f:pnpm --filter @objectstack/runtime test: Test Files 443 passed (443). Tests 7055 passed, 19 skipped (7074).pnpm --filter @objectstack/service-datasource test: Test Files 57 passed (57). Tests 975 passed (975).typecheckof both packages: exit 0.eslint --no-inline-configon the touched files: exit 0.f3e50da5f, each exit 0:check-adr-0087-registration(one declared-breaking changeset, dispositionnot-required (no-migration-prescription)),check-changeset-no-major,check-empty-changeset,check:error-status-conformance,check:nul-bytes,check:doc-authoring.dispatch-gates --ran), last run on71d6c23c4: 72 families derived, 71 run, 1 NOT-MEASURED. That one ischeck:dual-build-cjs-loads, exit 3: it needs every package'sdist/, and CI'sLint & Repo Gatesruns it.200(expected 200 to be 404).scripts/ablation-replace.mjsin wrap mode. Each mutation landed on disk with the anchor count going 1 → 0, and each was restored to blob == HEAD withgit diff HEADempty. Subjects are imported fromsrc, so nodistleg applies.flowIsUnknownoff: 3 failed, 1 passed.packageIsUnknownforced false: 3 failed, 3 passed.unknownToEngine && !rowDeletedoff: 2 failed, 2 passed.packageIsUnknownoff: 2 failed, 8 passed.unhideErrorguard deleted: 1 failed, 11 passed. A first attempt with an empty replacement was refused by the tool as a no-op and is not counted.Acceptance notes
external-datasourceby-name doors (GET /api/v1/datasources/:name/remote-tables,POST …/:name/test,POST …/:name/object-draft) are left as they are.external-datasource-service.tsrecords not throwing for an unknown datasource as deliberate: whether an unknown datasource is an error there is a question about the driver registration, and that service's comment declines to make it a throw.PublishPackageDraftsResponseSchema.outcome's describe saysnothing_to_publishmeans "the package had no pending drafts". That is still true at the protocol; the REST door now answers404before an unknown package reaches it. No spec text is changed here (Metadata-protocol: none).🤖 Generated with Claude Code
https://claude.ai/code/session_01X15AahTqrmeh8gAVdpKMCK
Generated by Claude Code