|
| 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