fix(core): v3 list actions accepted a filter shape that crashed them - #403
Merged
Conversation
`TypeCallParamsV3.filter` accepts both the v3 array of triples and the v2 object
dialect, kept for backward compatibility. That union is right for a single
`call`, which forwards the filter untouched. It is wrong for `callList` and
`fetchList`, which emulate keyset pagination by appending
`[cursorIdKey, '>', cursor]` to the filter on every page — an array is not a
preference there, it is the only shape the mechanism can extend.
So `b24.actions.v3.callList.make({ filter: { '>id': 100 } })` — a shape the
public type allows and the documentation promises — threw `filter is not
iterable` from a spread, one page into the walk. Confirmed rather than reasoned
about: spreading a plain object throws `is not iterable` at runtime.
The two list actions now narrow `filter` to `TypeFilterV3` in their own options
type, so the mistake is a compile error rather than a crash, and a runtime guard
catches the JavaScript caller's version with a code and a message that names the
fix. This is not a behaviour change anyone can be relying on: the path it closes
could only ever throw.
Also names the v2 batch request envelope. `TypeHttp.call` types its params as
`TypeCallParams`, and the batch request rides through it — `{ halt, cmd }` is
neither a filter nor a select, and it type-checks only because of the permissive
index signature. `BatchRequestEnvelopeV2` gives that shape a name at the site
that builds it. On the v3 side there is no envelope at all — the commands are
the request body — so the cast there stays, with a comment saying why rather
than looking like an oversight. Same class as the response-envelope mistyping
fixed in #395, the other direction.
Found while measuring the `TypeCallParams` options for #279.
Refs #279
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F22e2ft66y7nuBJjzdThBr
QA proved the message test was passing vacuously. It used `.catch(err => expect(...))` on a promise expected to reject — and when the promise RESOLVES the callback never runs, no assertion executes, and vitest reports green. Demonstrated by neutering the guard AND making the spread object-tolerant so the call succeeds: the `rejects.toThrow` test above went red while the `.catch` one stayed green. A test for an error message has to fail when there is no error. Now `rejects.toMatchObject`, plus a second case pinning that `fetchList` reports the same CODE and not merely the same class — without the guard that path throws too, just a TypeError. The guard itself shipped duplicated verbatim in both list actions. The repo has a tested position against exactly that: `baseStage` existed three times and the suite was pinning behaviour no shipped recipe had (#64a). One copy now, in `_keyset-paginate.ts`, which both actions already import from. Its doc comment also overclaimed. Both option types already narrow `filter` to `TypeFilterV3`, so for a TypeScript caller the `asserts` signature adds nothing the parameter type has not already done — it exists for the callers the types cannot reach, JavaScript and `params as any`, and now says so. Recorded there too: `callTail` / `fetchTail` do NOT need the guard, because they paginate through the separate `cursor` parameter and forward `filter` untouched. That is why the fix stops at two of the four v3 walkers, and it is worth writing down rather than leaving the next reader to re-derive. From the security review: `AjaxError` runs `requestInfo` through `redactSensitiveParams`; `SdkError` has no equivalent step for its `description`. Nothing keeps a credential out of it except callers not putting one there. The guard honours that by construction, and the expectation now sits on `SdkErrorDetails.description` next to the similar warning `originalError` already carries, so it reaches every caller rather than this one site. Docs: dropped a hard-coded "before v2.2.0" that presupposed a release number not yet decided, and documented `BatchRequestEnvelopeV2` in `70.core-http.md` — it is publicly exported, so leaving it undescribed made it a mystery symbol, and `check-api-reference-index` cannot catch that because it checks value exports. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F22e2ft66y7nuBJjzdThBr
8 tasks
…idated CI caught what I did not: `docs-lint --strict` went red on nine pages whose `audited:` stamp predates the JSDoc I added to `sdk-error.ts`. It passed locally because the check reads `git log -1` on the cited source, and at the moment I ran it that file was modified but not yet committed — so the lint still saw the old date. The lesson is procedural: `docs-lint --strict` has to be re-run AFTER committing, not before, or it reports on a state that no longer exists. Checked rather than rubber-stamped. The change to `sdk-error.ts` was a comment on `description`, no API movement, so the nine pages remain accurate and the stamps are honest. Two real gaps found while looking at them: `JSSDK_ACTION_V3_LIST_FILTER_NOT_ARRAY` was missing from the code table in `6.errors.md`. That page says codes are stable strings to match on, so shipping a new one without listing it there leaves callers matching on something the reference does not admit exists. Added, with the reason it fires and the note that `callTail` / `fetchTail` are unaffected. The same page now records that `SdkError`'s `description` is NOT redacted, while `AjaxError`'s `requestInfo` is. It already explained the `originalError` protection in detail, so the asymmetry belonged next to it — anyone constructing an `SdkError` themselves needs to know the guarantee stops at `AjaxError`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F22e2ft66y7nuBJjzdThBr
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two bugs found while measuring the
TypeCallParamsoptions for #279. Both non-breaking, both belong in2.xrather than waiting for the 3.0.0 batch.1. A documented filter shape crashed the v3 list actions
TypeCallParamsV3.filteraccepts the v3 array of triples and the v2 object dialect, kept deliberately for backward compatibility. That union is right for a singlecall, which forwards the filter untouched.It is wrong for
callList/fetchList. Those emulate keyset pagination by appending[cursorIdKey, '>', cursor]to the filter on every page:An array is not a preference there — it is the only shape the mechanism can extend. So:
…on a shape the public type allows and the documentation promises. Confirmed by running it, not by reading it:
Fix. The two list actions narrow
filtertoTypeFilterV3in their own options type, so the mistake becomes a compile error instead of a crash. A runtime guard catches the JavaScript caller's version of the same mistake and throwsSdkErrorwithJSSDK_ACTION_V3_LIST_FILTER_NOT_ARRAYand a message naming what to write instead.Not a behaviour change anyone can be relying on: the path this closes could only ever throw. Five tests, and the guard is mutation-verified — removing it brings back
filter is not iterable.2. The v2 batch request envelope had no name
TypeHttp.calltypes itsparamsasTypeCallParams, and the batch request rides through it.{ halt, cmd }is neither a filter nor a select; it type-checks only because of the permissive index signature.BatchRequestEnvelopeV2gives the shape a name at the site that builds it.On the v3 side there is no envelope at all — the commands are the request body — so the cast there stays, now with a comment saying why rather than looking like an oversight.
Same class as the response-envelope mistyping fixed in #395, from the other direction.
Context: this settles #279's blocking item
While measuring, two things about #279 turned out to be stale, both recorded in a comment there:
params['filter'] = 123compiles" — is no longer true. docs(jsdoc) + types: fill @todo docs (#154) and type request-side filters (#153) #280 madeTypeCallParamsV2/V3an intersection, and in an intersection the declared property beats the inherited index signature. Both element and dotted access are already rejected.docs/content,skills/andplaygrounds/:id31×,entityTypeId16×, thenfields,taskId,iblockId,PLACEMENT… against 14filter.crm.item.getis{ entityTypeId, id }.The decision taken: keep the index signature, narrow its value type from
anytounknownin 3.0.0 (measured cost: 3 sites, of which one is inside the deprecatedcallMethodthat #277 deletes anyway, and the other two are the bug fixed here). Per-method parameter types are the only option that would catch afitlertypo, and that is a separate project rather than a checklist item.With that settled, #279 stops blocking the 3.0.0 scope freeze.
Checks
pnpm run typecheck— all eight passes, 0 errorsjsSdk:unit+jsSdk:types+skills:unitlint(1 pre-existing unrelated warning indocs/server/api/ai.post.ts),lint:md,docs-lint --strict,md-internal-links,check-api-reference-indexDocs
2.call-list-rest-api-ver3.mdand2.fetch-list-rest-api-ver3.mddescribedparamsasOmit<TypeCallParams, 'pagination' | 'order'>, which is now stale, and neither mentioned that the object filter cannot work here. Both updated, with the reason rather than just the rule.Refs #279
🤖 Generated with Claude Code
https://claude.ai/code/session_01F22e2ft66y7nuBJjzdThBr
Generated by Claude Code