Skip to content

Commit 340c5e5

Browse files
os-litantclaude
andauthored
fix(rest): /analytics/dataset/query carries the producer's userMessage on its three hand-built terminals (#12731)
The route builds its error envelopes by hand and shares no exit with the /data door, so #9934's producer-marked `userMessage` — applied there once at the exit through `withDeclaredUserMessage`, branch-agnostically — was applied at none of them. Scope is by ARM: ① (declared 4xx passthrough), ③a (declared 5xx relay) and ③b (generic 500) all dropped it; ①b already carried it, because its body comes from `resolveErrorResponse`, and it is deliberately untouched. The value is `boundedDeclaredUserMessage` (#12693), resolved once for the whole catch. Exactly one optional key is added, only when the producer marked one; no existing key moves or changes value at any arm. Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd Co-authored-by: Claude <noreply@anthropic.com>
1 parent 30928a6 commit 340c5e5

3 files changed

Lines changed: 524 additions & 3 deletions

File tree

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
---
2+
"@objectstack/rest": patch
3+
---
4+
5+
fix(rest): `/analytics/dataset/query` carries a producer-marked `userMessage` on its three hand-built terminals (#12710)
6+
7+
`POST /api/v1/analytics/dataset/query` (and its environment-scoped twin) builds
8+
its error envelopes by hand and shares no exit with the `/data` door, so #9934's
9+
producer-marked `userMessage` — a channel that door applies once at its exit,
10+
branch-agnostically, through `withDeclaredUserMessage` — was applied at none of
11+
them. A producer's caller-facing sentence reached the client on
12+
`POST /data/:object` and vanished here for the identical throw.
13+
14+
**Scope is by ARM.** Four terminals live in that route's catch; three dropped
15+
the mark and one did not:
16+
17+
| arm | envelope | before |
18+
| :-- | :--- | :--- |
19+
| ① declared 4xx ADR-0112 passthrough | hand-built `{ code, message }` | ⛔ no mark |
20+
| ①b `classifiedRefusalAnswer` re-dress | `{ ...refusalFields, message }` | ✅ carried it |
21+
| ③a declared 5xx relay | `declaredServerFaultAnswer`'s body, sent verbatim | ⛔ no mark |
22+
| ③b generic `500 ANALYTICS_QUERY_FAILED` | hand-built `{ code, error }` | ⛔ no mark |
23+
24+
①b already carried it because its body comes from `resolveErrorResponse`, whose
25+
arms ride the mark already. The other three hold no classification to ride on.
26+
27+
Measured on `4af6c4419` before the repair, one marked producer per arm, driven
28+
through the real route against the flat `/data` door for the identical throw:
29+
30+
```text
31+
throw { code: 'INVALID_FILTER', status: 400, userMessage: 'Check the filter…' }
32+
① analytics : 400 {"code":"INVALID_FILTER","message":"…"} — no mark
33+
/data door : 400 {"error":"…","code":"INVALID_FILTER",
34+
"userMessage":"Check the filter…"} — mark carried
35+
36+
throw { code: 'READ_SCOPE_COMPILE_FAILED', status: 500, userMessage: '…' }
37+
③a analytics: 500 {"error":"Internal server error",
38+
"code":"READ_SCOPE_COMPILE_FAILED"} — no mark
39+
/data door : 500 {…, "userMessage":"…"} — mark carried
40+
41+
throw Error('[Analytics] no strategy can handle query …') + userMessage
42+
③b analytics: 500 {"code":"ANALYTICS_QUERY_FAILED","error":"…"} — no mark
43+
/data door : 500 {"code":"INTERNAL_ERROR","userMessage":"…"} — mark carried
44+
```
45+
46+
Nothing invalid shipped — every body parsed as `ApiErrorSchema`, which already
47+
declares the optional field — and that is what made the loss silent and
48+
one-directional: a console told by ADR-0112 to render `userMessage` verbatim
49+
found nothing at these three arms and fell back to its generic substitution, for
50+
the same throw the twin door rendered.
51+
52+
**What callers see change:** exactly one optional key is ADDED, and only when
53+
the producer marked one. No existing key moves or changes value, at any of the
54+
four arms — pinned as an explicit key-order assertion per arm for an unmarked
55+
producer.
56+
57+
The value comes from `boundedDeclaredUserMessage` (exported by #12693) —
58+
`declaredUserMessage`'s presence answer with #5423's bound applied — resolved
59+
once for the whole catch rather than at each terminal, so this door has one
60+
answer to "is there a mark, and how long may it be" and shares it with `/data`
61+
rather than copying it. ①b is deliberately untouched: a second application there
62+
would be one rule applied twice.
63+
64+
**Unchanged:** the prose withhold (#5367/#5437/#5811) — a declared server fault's
65+
message is still replaced by the generic sentence and still reaches the operator
66+
in full through the `logError` line that runs before every arm; the statuses and
67+
`code`s all four arms answer; and #5667's tiering, which leaves a self-authored
68+
undeclared fault readable.
69+
70+
**Not reachable from in-repo producers today.** Censused at claim: no package
71+
under `packages/services/**` sets a `userMessage` of any kind, and
72+
`service-analytics` dispatches no sandbox hook, so the QuickJS side-channel — the
73+
other in-repo carrier — does not reach this door either. This wires up a declared
74+
channel the published contract already promises on this route's envelope; the
75+
intended producer is an app author's analytics datasource or strategy.

0 commit comments

Comments
 (0)