datasources: GET /datasources/NAME/remote-tables and POST /datasources/NAME/object-draft answer an unknown name 400 EXTERNAL_DATASOURCE_ERROR where GET /datasources/NAME answers 404 (the #25486 / #27510 family) #27618
Description
domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw
Filing gate: ① a product defect with a reproduction. While fixing #27510, the dev found that two more by-name datasource doors answer an unknown name as a malformed request where their sibling answers 404. It was measured once over HTTP.
reach: measured at the public datasource admin doors at objectstack-ai/objectstack b09ea62c76, with a throwaway probe over the real composition: a real ObjectQL, the connection service, the admin service, the ExternalDatasourceServicePlugin wiring, and the admin routes on Hono, called by an entitled admin. The probe was not committed. For the same unknown name no_such_ds:
GET /api/v1/datasources/no_such_ds(control) answers 404RESOURCE_NOT_FOUNDDatasource "no_such_ds" does not exist.GET /api/v1/datasources/no_such_ds/remote-tablesanswers 400EXTERNAL_DATASOURCE_ERRORDatasource 'no_such_ds' has no datasource definition, so nothing declares it a federation source: ADR-0015 reserves federation for a datasource whose schemaMode is not 'managed', …POST /api/v1/datasources/no_such_ds/object-draftwith body{"table":"orders"}answers 400EXTERNAL_DATASOURCE_ERRORwith the same message.
Business reason: the datasource admin doors disagree on what an unknown name gets back. The Sync flow's two doors answer it 400 with a lecture on federation and schemaMode. That tells an admin tool, script or AI caller that its request was malformed, or that it should re-declare the datasource as external, when the datasource does not exist. Every other by-name door answers 404, and since #27574 the Test door does too.
Reader who acts: the triage seat, then the implementer for packages/services/service-datasource, which owns ExternalDatasourceService and the admin routes, and possibly packages/rest for the federation twins below.
Dedupe: searched the titles of the last 100 issues and PRs (#27515 to #27614, open and closed) for remote-tables, remote tables, object-draft, object draft, federation, unknown name and 404. One hit: #27574, the PR for #27510, which fixed the Test door only and lists these two doors as out of scope. No existing card covers them. Same family as #25486 (closed) and #27510.
Back-link: #27510
⛔ Not a claim. Grading is triage's.
Symptom
GET /api/v1/datasources/NAME/remote-tables and POST /api/v1/datasources/NAME/object-draft answer an unknown name 400 EXTERNAL_DATASOURCE_ERROR ("has no datasource definition, so nothing declares it a federation source …"). Their sibling GET /api/v1/datasources/NAME answers the same name 404 RESOURCE_NOT_FOUND.
Reproduction
As an admin holding manage_platform_settings, on any deployment with no datasource named no_such_ds:
GET /api/v1/datasources/no_such_dsreturns 404RESOURCE_NOT_FOUND,Datasource "no_such_ds" does not exist.GET /api/v1/datasources/no_such_ds/remote-tablesreturns 400EXTERNAL_DATASOURCE_ERROR,Datasource 'no_such_ds' has no datasource definition, so nothing declares it a federation source: …POST /api/v1/datasources/no_such_ds/object-draftwith{"table":"orders"}returns 400EXTERNAL_DATASOURCE_ERRORwith the same message.
Expected
404 RESOURCE_NOT_FOUND in GET's words (Datasource "NAME" does not exist.) for a name that resolves no saved datasource, as #27574 does for POST /api/v1/datasources/NAME/test. The 400 refusals stay for a datasource that exists and is not a federation source: a managed one, or the primary datasource.
Mechanism
ExternalDatasourceService.assertFederationSource(datasource, door) (packages/services/service-datasource/src/external-datasource-service.ts) reads this.config.getDatasource(datasource). That is the same metadata.get('datasource', name) read the sibling doors make. Then isFederationSource(undefined) is false, so it throws notAFederationSourceError(datasource, undefined, door) with the door's 400 code, and the absent definition is folded into "not a federation source". Every federation door runs this guard first, so the same shape is expected, though not measured, on:
- the federation twins in
packages/rest/src/external-datasource-routes.ts:GET /datasources/NAME/external/tables,POST …/external/tables/REMOTE/draft, andPOST …/external/tables/REMOTE/import(whose door code isEXTERNAL_IMPORT_ERROR); refreshCatalog(POST …/external/refresh-catalog), if it goes through the same guard.
The fix splits "no definition" from "a definition that is not a federation source" inside assertFederationSource. It throws the 404 envelope for the first case, and the routes' existing badRequest relay in admin-routes.ts already answers that as 404. The packages/rest twins need their own relay check.
Reviewer's should-fix, carried here
The contract review of #27574 noted that datasourceNotFoundError now has two private copies: one in datasource-admin-service.ts (used by update, remove and migrate-credential) and one in external-datasource-service.ts (used by Test). The fix for this card adds a third caller, so it is the natural place to share one helper and keep every by-name door's 404 in one spelling.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 29, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: 可对外的 API | integration-system.datasource-admin-lifecycle | P②
Triage: p2 ·
domain:server·area:api· 2026-10-29T15:15Z⛔ Not a claim.
assertFederationSourcesplits "no definition" (404, the one shareddatasourceNotFoundError) from "a definition that is not a federation source" (400, unchanged).packages/restfederation twins andrefresh-catalogare measured and pinned in the same PR. Both packages are server lane. The fix(service-datasource)!: POST /datasources/:name/test answers an unknown name 404 RESOURCE_NOT_FOUND #27574 should-fix (one helper, not three copies) rides along.bug,pm:queue.Generated by Claude Code
objectstack-fleet commentedon Oct 29, 2026
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
Claim: PM loop round 3 · card thread
Session:
session_01L6bhgUCizoYWQVJWjhdR6F(the seat); the card thread issession_01L6bhgXTXpQTz45k8v4WACtAccount:
os-salesBranch:
claude/project-thread-bvrf7w(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree:
objectstack-issue-27618in the card thread's own containerDomain:
domain:serverSeat:
domain:server#2File surface:
packages/services/service-datasource/src/external-datasource-service.ts(assertFederationSource, the not-found helper),packages/services/service-datasource/src/datasource-admin-service.ts(the second not-found copy), the service's admin-routes relay if it answers these doors,packages/rest/src/external-datasource-routes.ts(answerThrown), and tests (stop on breach; explain in the report)Container & model:
M,mode:thread,model: default (dispatch-gates --tier: no path-derived mandate),effort: highClause-②: no
Metadata-protocol: none
Responsibility:
the datasource federation doors (this lane) | GET /datasources/NAME already answers 404 for an unknown name | any admin or agent addressing a datasource by nameThread-read: 6500612695
Premise: holds (read on main 2026-10-29T18:16Z: an absent definition is folded into the non-federation 400; the rest twins send every non-sandbox fault as 400; PR #27574 merged)
Serial constraints cleared: none (no open PR edits these files).
objectstack-fleet commentedon Oct 29, 2026
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
os-dev-report
{ "issue": 27618, "status": "done", "branch": "claude/project-thread-bvrf7w", "pr": "https://github.com/objectstack-ai/objectstack/pull/27693", "session": "session_01L6bhgXTXpQTz45k8v4WACt (card thread; seat session_01L6bhgUCizoYWQVJWjhdR6F)", "premise_still_valid": true, "summary": "assertFederationSource now throws the 404 RESOURCE_NOT_FOUND envelope for a name with no datasource definition and keeps the 400 (door code) for a definition that is not a federation source; the primary datasource keeps its own 400. The two private datasourceNotFoundError copies are one helper in datasource-admin-service.ts (module export only, package index unchanged, so no public-surface change); GET /datasources/NAME takes its words from it. service-datasource admin routes already relayed the envelope; rest answerThrown now relays 404/RESOURCE_NOT_FOUND, so external/tables, tables/REMOTE/draft, tables/REMOTE/import and refresh-catalog answer an unknown name as GET does. Two pins that held the folded 400 moved (managed-datasource-sync-refusal ghost case, external-metadata-read-at-use no-metadata case). Head 2d19915c (fix 88e7fb01 merged with origin/main 50d05cb1).", "tests": "Red first: 2 new service-datasource 404 pins and 4 new rest 404 pins red on main (400 EXTERNAL_DATASOURCE_ERROR / EXTERNAL_IMPORT_ERROR), controls green. Green on 2d19915c: service-datasource test+typecheck under lock VERDICT command-exit 0, Test Files 61/61, Tests 1010/1010; rest test+typecheck under lock VERDICT command-exit 0, Test Files 340/340, Tests 6674 passed 326 skipped. Ablation (scripts/ablation-replace.mjs, fix committed first; both suites read src via relative import / rest vitest alias, no dist leg): leg 1 delete the no-definition throw: service-datasource 2 failed 4 passed, rest 4 failed 8 passed, restored blob 922e07c2df3d == HEAD; leg 2 delete rest's 404 relay: rest 4 failed 8 passed (received 400), restored blob 1629a50c6b7e == HEAD; git diff HEAD empty after each.", "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack at 2d19915c: 62 derived, 62 run, each exit 0 (check:dual-build-cjs-loads first exit 3 PREREQUISITE, 48 dists missing; after full turbo build exit 0, 112 entries/69 packages). --ran reconcile: 62 run, 0 NOT-MEASURED (derived zero). eslint --no-inline-config on the 8 changed files exit 0; pnpm lint (repo-wide) exit 0. CI check-runs on PR 27693: in_progress at report time.", "dogfood_battery": "n/a — no ! in the PR title and Clause-②: no without a (narrowing) arm", "mcp_calls": "4 — reads only: issue_read get x1, issue_read get_comments x2, pull_request_read get_files x1 (PR 27574). No MCP write tool.", "api_writes": "2 — relay pr_create (workflow dispatch, run 40937666526, read-back identical 8558 bytes, assignees none) opening PR 27693; post-stamped comment on issue 27618 (this report). git push is not a REST write.", "open_questions": [], "out_of_scope_findings": [ "observation, not filed: POST /datasources/NAME/external/validate does not run the federation guard, so an unknown name answers 200 with an empty report (code reading only, no door measured; no class a/b/c reach). carrier: none, noted in PR Acceptance notes", "deviation: PR body attribution lines name this card thread (session-context requirement) rather than the seat-2 thread line in order-common.md; footer carries this card thread's session id" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 29, 2026
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
ACCEPT → PR #27693 comment 6506425655 · head
2d19915c· 2026-10-29T19:33Z · landing sent. Follow-up: #27696 (the validate door).