You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 67cf0b1
Browse filesBrowse the repository at this point in the historyBrowse files
fix(runtime)!: the dispatcher's GET /data/:object/:id refuses an unknown query parameter as RestServer does (#26947)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XF3QFJAFuZhRRxEVZmmSGCV)_Fixes#26898
Clause-②: yes (narrowing)
Metadata-protocol: none
Skills-docs: updated here — `content/docs/api/data-api.mdx` (the by-id
read's parameter sentence said "all other parameters are discarded"); no
`skills/**` file teaches this route's query set
## 维护者速读
- **改了什么**:运行时 dispatcher 的 `GET /data/:object/:id` 遇到 `select` /
`expand` 以外的查询参数,改为 `400 VALIDATION_ERROR` 拒绝,与 RestServer 同状态、同
code、同一句文案;两边读同一个参数集 `DATA_RECORD_READ_PARAMS`(由 `@objectstack/rest`
导出)。
- **为什么改**:同一个拼错的参数(`?selct=name`)在 RestServer 上被拒,在 dispatcher
上被静默丢掉并返回全部列,客户端得不到任何提示。
- **风险与回滚**:原先被容忍的多余参数现在会 400(只影响 dispatcher 承载的按 id 读取);官方 SDK 的
`data.get` 不带查询参数,不受影响。回滚 = revert 本 PR。
- **先看**:`packages/runtime/src/domains/data.ts`(按 id
读取分支的新拒绝与门序)、`packages/runtime/src/domains/data-record-read-status-parity.test.ts`(两宿主对拍)、`packages/rest/src/index.ts`(新增的一个导出)。
- **两分钟验证**:`pnpm --filter @objectstack/runtime exec vitest run
--maxWorkers=2 src/domains/data-record-read-status-parity.test.ts`,预期
19/19 通过,其中 `?nope=1` 各例两宿主都是 400。
- **需要你做的**:无,席位落地。
## What changes
- **The dispatcher's by-id read asks the route's closed set.**
`packages/runtime/src/domains/data.ts`: after the object's API exposure
gate (`refuseUnexposedDataAction(..., 'get', ...)`, where `RestServer`
asks `enforceApiAccess`), the branch asks
`unknownQueryParamRefusalMessage(query, DATA_RECORD_READ_PARAMS)` and
answers any name outside `select` / `expand` `400 VALIDATION_ERROR` in
the dispatcher's envelope, the same sentence `RestServer`'s
`refuseUnknownQueryParams` writes. It used to drop those names and serve
the read (`200`). The refusal comes before the read, so it is one answer
for an existing record, a missing id and a row the caller may not read.
- **Where the one set lives, and why.** `DATA_RECORD_READ_PARAMS` stays
where it was declared (`packages/rest/src/rest-server.ts`, #7606 / #8039
docblock) and `@objectstack/rest` now exports it from its entry.
`@objectstack/runtime` already depends on `@objectstack/rest`
(`packages/runtime/package.json`) and `data.ts` already imports two
`/data` rules from it (`assertFilterParamSuppliedOnce`,
`repeatedQueryParamMessage`); `@objectstack/rest` does not depend on
`@objectstack/runtime`. So no new dependency edge and no cycle.
`packages/spec/**` is untouched. The precedent is #23072
(`metaItemQueryRefusal`) and #26020 (`unknownQueryParamRefusalMessage`):
a closed-set decision `RestServer` owns, exported for the dispatcher's
twin.
- **Docs.** `content/docs/api/data-api.mdx` said the by-id read discards
other parameters; it now says they are refused, naming them.
- **Changeset.** `patch` on `@objectstack/runtime` and
`@objectstack/rest`, `fix(runtime)!:`, BREAKING banner, ADR-0087
`not-required (no-migration-prescription)`.
**Clause-② measured on the diff.** The claim carried `no (narrowing)` as
likely. The diff narrows one door's accept set (the `(narrowing)` arm)
and also adds one name to `@objectstack/rest`'s entry, which the
criterion counts as a wider public surface ("本卡放宽接受集或扩大公开面吗",
`references/execution-duties.md`). So the measured line is `yes
(narrowing)`, the line #23072's PR (8024e12) declared for the same
shape. If the seat reads a re-exported constant as not widening the
surface, the line becomes `no (narrowing)` and nothing else changes.
## Tests
- **Red first, on the base `57820085fa`** (test commit only, `data.ts`
at base): `data-record-read-status-parity.test.ts` 6 failed / 13 passed.
Every failure was the dispatcher assertion: `?nope=1`,
`?select=title&nope=1`, `?fields=title`, `?nope=1&selct=title` on the
own record answered `200`; `?nope=1` on the hidden row and the missing
id answered `404`. `RestServer` answered all six `400 VALIDATION_ERROR`.
- **Green after the fix**: 19 / 19. Both hosts answer the six cases `400
VALIDATION_ERROR` with the same sentence. Controls kept on both hosts:
`?select=title` and `?expand=parent` on the own record `200`, a missing
id `404`, a hidden row `404`, a hidden row `?select=title` `404`, the
#26816 `400` cases unchanged.
- **Gate order, dispatcher pin**: `?nope=1` on an object with
`apiEnabled: false` answers `404 OBJECT_API_DISABLED`, the same answer
as without the parameter. Pinned on the dispatcher alone: this rig's
`RestServer` has no metadata store, so its exposure gate falls open
there (measured: a plain read of that object answered `404
RECORD_NOT_FOUND`) and would not measure the order. `RestServer`'s order
is `enforceApiAccess` before `refuseUnknownQueryParams` in
`crud-routes.ts`.
- **Ablation** (`scripts/ablation-replace.mjs`, `if (unknownParams !==
undefined)` → `if (false && ...)`, blob `a351a771` → `992a9a63`): the
six card cases red, 13 passed; restored, blob == HEAD and `git diff
HEAD` empty. The test reaches `data.ts` through a relative source
import, so no build sits between the mutation and the run.
- **Package suites** (under the verify lock): `@objectstack/runtime`
test 1 failed / 462 files on the first run: `http-dispatcher.test.ts`
pinned the old drop ("should NOT pass non-allowlisted params"). That pin
now asserts the refusal (`400 VALIDATION_ERROR`, the sentence naming
`filter` and `malicious`, `getData` not asked):
`http-dispatcher.test.ts` 248 / 248. `@objectstack/runtime` typecheck 0,
`@objectstack/rest` test 329 / 329 files (6525 passed), typecheck 0.
- **Lint** on the touched files: `eslint --no-inline-config` exit 0.
## Acceptance notes
- **The dispatcher's other `/data` routes** (`route-ledger.ts` rows),
each against its `RestServer` twin. None has a declared closed set on
the `RestServer` side, so none is fixed here.
- `GET /data/:object` (list): open by design on both hosts; a leftover
key is an implicit field filter and an unknown field is refused `400
INVALID_FIELD` one layer down (`query-allowlist.ts` "do not add this
gate").
- `POST /data/:object/query`, `POST /data/:object`, `PATCH
/data/:object/:id`: neither host reads the querystring; an unknown
parameter is ignored on both. No split.
- `DELETE /data/:object/:id`: both read `expectedVersion` (once,
arity-gated on both) and ignore every other name. No split.
- Closing those four would be a new closed set on both hosts at once, a
#7606-policy question, not this card's parity defect.
- **An observation, not filed.** A host whose `/data` is answered by the
dispatcher alone (no `RestServer`) has no `GET /data/:object/export`
route, so that path reaches the by-id branch with id `export`. It
answered `404 RECORD_NOT_FOUND` before; with `?format=csv` it now
answers this route's `400` naming `format`. Both are refusals of a route
that host does not serve; not measured on a live host.
- **The wire carries no `param` key** on either host for this refusal:
`RestServer` writes `{ error: { code, message } }`, the dispatcher `{
success: false, error: { code, message } }`. The refused names are in
the sentence, identical on both (the rig asserts the messages equal).
## Verification
All on the final head `f5f4f537`.
<details><summary>Derived gate families: 89 derived, 89 run, all exit
0</summary>
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 89 families over the 6 changed paths
(merge base `57820085f`). Each was run with its exit code captured
before any pipe; `--ran` reconciles: "89 derived famil(ies) accounted
for — 89 run, 0 NOT-MEASURED (a DERIVED zero — all 89 recorded an exit
code and none of them is 3)".
- Four first answered `exit 3` (prerequisite, nothing measured) and were
re-run to `exit 0`: `check-plugin-teardown-shape --self-test` (after
`git fetch --depth=1` of its pinned fixture commit),
`check:type-check-debt` (run under the verify lock: "126/129 workspace
packages type-checked, 0 in the DEBT ledger"), `check:skill-examples`
and `check:dual-build-cjs-loads` (after that gate's closure build: "112
published require entry point(s) across 69 package(s) load").
- Re-run after the last commit (a test-file change):
`check:test-source-alias`, `check:cross-package-test-inputs`,
`check:engine-double-contract`, `check:tier-file-adoption`,
`check:rest-log-spy-declared`, `check:registry-log-declared`,
`check:nul-bytes`, `check:issue-citations`, `check:type-check-debt`: all
exit 0.
- Scoped lint: `eslint --no-inline-config` on the four touched source
and test files, exit 0. The repo-wide `pnpm lint` is CI's.
- `test:repo` is not run locally (CI's `Test Core` runs it); the diff
touches no file of that project.
</details>
<details><summary>Dogfood battery (the narrowing arm): 334 / 334 files
at f5f4f53</summary>
- Closure built with `pnpm --filter '@objectstack/dogfood^...' build`,
then `pnpm --filter @objectstack/dogfood test` under the verify lock, no
shard, no path, on `f5f4f537`.
- `os-verify-lock: VERDICT command-exit 0 · held the lock 1748s`; `Test
Files 334 passed | 3 skipped (337)`; `Tests 2716 passed | 20 skipped
(2736)`. No failure.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5oqbhjkiqCJ8i3DzzU)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(runtime)!: the runtime dispatcher's `GET /data/:object/:id` refuses a query parameter other than `select` / `expand` with `400 VALIDATION_ERROR`, as `RestServer` does, instead of dropping it and answering `200`
7
+
8
+
Clause-②: yes (narrowing) — the dispatcher's by-id read refuses a query parameter it dropped before, and `@objectstack/rest` gains one export (`DATA_RECORD_READ_PARAMS`) so both hosts read one set.
9
+
Metadata-protocol: none
10
+
11
+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: no spec key, authorable spelling or stored shape is added, removed, renamed or re-shaped, and no exported declaration is removed or narrowed. `@objectstack/rest` gains one export, `DATA_RECORD_READ_PARAMS`, the closed set its own route already asked. What narrows is one runtime door's query accept set: a parameter the dispatcher's by-id read dropped now answers a located `400 VALIDATION_ERROR`, and the remedy is in the request a caller sends, never a rewrite of a package source or a stored row. The other categories are closed on facts: both packages publish (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and nothing exported is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
12
+
13
+
**BREAKING** accept-set narrowing at one runtime dispatcher door, the by-id record read served by the `@objectstack/hono` catch-all and `dispatcher-plugin`.
14
+
15
+
-**Before.**`GET /api/v1/data/:object/:id?selct=name` answered `200` with every column on the dispatcher: the branch kept `select` and `expand` and silently dropped every other name. `RestServer` refused the same request `400 VALIDATION_ERROR`, so a typo was ignored on one host and refused on the other.
16
+
-**Now.** The dispatcher asks the route's closed set, `DATA_RECORD_READ_PARAMS`, imported from `@objectstack/rest`, the same set `RestServer` asks, and refuses any other name `400 VALIDATION_ERROR` with `RestServer`'s sentence: it names each refused parameter and lists `expand, select`. The refusal comes before the read, so it is the same answer for an existing record, a missing id and a row the caller may not read.
17
+
-**Unchanged.**`select` and `expand` are read as before. An object the API does not expose is answered its exposure refusal first, before the query is judged: the dispatcher's `404 OBJECT_API_DISABLED` with or without the unknown name, and `RestServer` likewise asks its closed set only after `enforceApiAccess`. The other `/data` routes are not touched.
18
+
19
+
**The remedy.** Send only `select` and `expand` to the by-id read. `fields` and `populate` are not synonyms on this route (#8039): spell them `select` and `expand`.
Copy file name to clipboardExpand all lines: content/docs/api/data-api.mdx
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -212,7 +212,7 @@ raw rows** with nothing to say the aggregation never happened.
212
212
213
213
### `GET /data/:object/:id`
214
214
215
-
Get a single record by ID. Only `select` and `expand` query parameters are allowed; all other parameters are discarded.
215
+
Get a single record by ID. Only `select` and `expand` query parameters are allowed; any other parameter is refused `400 VALIDATION_ERROR`, and the message names it and lists the two accepted ones.
0 commit comments