From 0458b426d032d79b978c713ac73afaced1aeae00 Mon Sep 17 00:00:00 2001 From: Shevchik Igor Date: Wed, 26 Aug 2026 11:16:59 +0000 Subject: [PATCH 1/3] fix(core): v3 list actions accepted a filter shape that crashed them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 Claude-Session: https://claude.ai/code/session_01F22e2ft66y7nuBJjzdThBr --- .../2.call-list-rest-api-ver3.md | 3 +- .../2.fetch-list-rest-api-ver3.md | 5 +- .../jssdk/src/core/actions/v3/call-list.ts | 40 +++++- .../jssdk/src/core/actions/v3/fetch-list.ts | 39 +++++- packages/jssdk/src/core/http/v2.ts | 15 ++- packages/jssdk/src/core/http/v3.ts | 5 + packages/jssdk/src/types/http.ts | 24 ++++ .../core/list-v3-filter-shape.unit.spec.ts | 121 ++++++++++++++++++ 8 files changed, 238 insertions(+), 14 deletions(-) create mode 100644 test/integration/core/list-v3-filter-shape.unit.spec.ts diff --git a/docs/content/docs/2.working-with-the-rest-api/2.call-list-rest-api-ver3.md b/docs/content/docs/2.working-with-the-rest-api/2.call-list-rest-api-ver3.md index b6065c48..488717f3 100644 --- a/docs/content/docs/2.working-with-the-rest-api/2.call-list-rest-api-ver3.md +++ b/docs/content/docs/2.working-with-the-rest-api/2.call-list-rest-api-ver3.md @@ -72,7 +72,7 @@ The `options` object contains the following properties: | Parameter | Type | Required | Description | |----|----|----|----| | **`method`** | `string`{lang="ts-type"} | Yes | REST API method name that returns a data list (e.g., `crm.contact.list`, `tasks.task.list`). | -| **`params`** | `Omit`{lang="ts-type"} | No | Request parameters, excluding the `pagination` and `order` parameters. The `pagination` parameter is reserved because the method retrieves all data in a single call. The `order` parameter is reserved because cursor-based pagination requires sorting strictly by `cursorIdKey` (which defaults to `idKey`) ascending — see [Limitations](#limitations). Use `filter` and `select` to control the selection. | +| **`params`** | `Omit & { filter?: TypeFilterV3 }`{lang="ts-type"} | No | Request parameters, excluding the `pagination` and `order` parameters, and with `filter` narrowed to the `restApi:v3` array form (see [Limitations](#limitations)). The `pagination` parameter is reserved because the method retrieves all data in a single call. The `order` parameter is reserved because cursor-based pagination requires sorting strictly by `cursorIdKey` (which defaults to `idKey`) ascending — see [Limitations](#limitations). Use `filter` and `select` to control the selection. | | **`idKey`** | `string`{lang="ts-type"} | No | Name of the id field **as it appears in each response item**; its value drives the cursor. Default: `'id'`. Set it to match the id field the method returns. | | **`cursorIdKey`** | `string`{lang="ts-type"} | No | Field name used in the **request** for `order` and the `[field, '>', n]` page filter. Defaults to `idKey`. Set it only when the sortable / filterable field name differs from the response field name (e.g. an uppercase request field but a lowercase response id): pass `idKey: 'id', cursorIdKey: 'ID'`. | | **`customKeyForResult`** | `string`{lang="ts-type"} | Yes | Custom key indicating that the REST API response will be selected by this field. For example: `items` for a list of CRM elements. | @@ -118,6 +118,7 @@ Some v3 list methods (e.g. `note.*`) also return a `nextCursor`{lang="ts-type"} - **Page size**: Bitrix24 REST API version 3 limitation — maximum `1000` records per request. - **Sorting is fixed**: The method always sorts by `cursorIdKey` (which defaults to `idKey`) ascending, because cursor pagination relies on `[cursorIdKey, '>', nextId]` filters to walk the dataset. A user-supplied `order` value would break that invariant, so the type signature excludes `order` and any value passed at runtime is stripped with a `warning` log entry. To narrow the result set, use `filter` instead. +- **`filter` must be the v3 array form**: `[['id', '>', 100]]`, or the output of `FilterV3.build(...)`. The `restApi:v2` object dialect (`{ '>id': 100 }`) is accepted by `TypeCallParamsV3`{lang="ts-type"} for backward compatibility and works with a plain `call`, but not here: cursor pagination appends `[cursorIdKey, '>', nextId]` to the same filter on every page, so an array is the only shape it can extend. Passing an object throws `SdkError`{lang="ts-type"} with code `JSSDK_ACTION_V3_LIST_FILTER_NOT_ARRAY`. Before v2.2.0 it was accepted and then failed mid-walk with `filter is not iterable`. - **Only for list methods**: Intended only for methods that return data arrays. ## Error Handling diff --git a/docs/content/docs/2.working-with-the-rest-api/2.fetch-list-rest-api-ver3.md b/docs/content/docs/2.working-with-the-rest-api/2.fetch-list-rest-api-ver3.md index 46f5e4aa..75048aa7 100644 --- a/docs/content/docs/2.working-with-the-rest-api/2.fetch-list-rest-api-ver3.md +++ b/docs/content/docs/2.working-with-the-rest-api/2.fetch-list-rest-api-ver3.md @@ -2,7 +2,7 @@ title: FetchListV3.make description: 'Returns an AsyncGenerator that allows processing data from list methods of Bitrix24 REST API version 3 as it is received without loading the entire array into memory at once. This is especially useful when working with very large volumes of data.' category: 'actions' -audited: 2026-08-18 +audited: 2026-08-26 restApiVersion: 'rest-api-ver3' navigation: title: FetchList @@ -70,7 +70,7 @@ The `options` object contains the following properties: | Parameter | Type | Required | Description | |----|----|----|----| | **`method`** | `string`{lang="ts-type"} | Yes | REST API method name that returns a data list (e.g., `crm.contact.list`, `tasks.task.list`). | -| **`params`** | `Omit`{lang="ts-type"} | No | Request parameters, excluding the `pagination` and `order` parameters. The `pagination` parameter is reserved because the method retrieves all data in a single call. The `order` parameter is reserved because cursor-based pagination requires sorting strictly by `cursorIdKey` (which defaults to `idKey`) ascending — see [Limitations](#limitations). Use `filter` and `select` to control the selection. | +| **`params`** | `Omit & { filter?: TypeFilterV3 }`{lang="ts-type"} | No | Request parameters, excluding the `pagination` and `order` parameters, and with `filter` narrowed to the `restApi:v3` array form (see [Limitations](#limitations)). The `pagination` parameter is reserved because the method retrieves all data in a single call. The `order` parameter is reserved because cursor-based pagination requires sorting strictly by `cursorIdKey` (which defaults to `idKey`) ascending — see [Limitations](#limitations). Use `filter` and `select` to control the selection. | | **`idKey`** | `string`{lang="ts-type"} | No | Name of the id field **as it appears in each response item**; its value drives the cursor. Default: `'id'`. Set it to match the id field the method returns. | | **`cursorIdKey`** | `string`{lang="ts-type"} | No | Field name used in the **request** for `order` and the `[field, '>', n]` page filter. Defaults to `idKey`. Set it only when the sortable / filterable field name differs from the response field name (e.g. an uppercase request field but a lowercase response id): pass `idKey: 'id', cursorIdKey: 'ID'`. | | **`customKeyForResult`** | `string`{lang="ts-type"} | Yes | Custom key indicating that the REST API response will be selected by this field. For example: `items` for a list of CRM elements. | @@ -127,6 +127,7 @@ Some v3 list methods (e.g. `note.*`) also return a `nextCursor`{lang="ts-type"} - **Page size**: Bitrix24 REST API version 3 limitation — maximum `1000` records per request. - **Sorting is fixed**: The method always sorts by `cursorIdKey` (which defaults to `idKey`) ascending, because cursor pagination relies on `[cursorIdKey, '>', nextId]` filters to walk the dataset. A user-supplied `order` value would break that invariant, so the type signature excludes `order` and any value passed at runtime is stripped with a `warning` log entry. To narrow the result set, use `filter` instead. +- **`filter` must be the v3 array form**: `[['id', '>', 100]]`, or the output of `FilterV3.build(...)`. The `restApi:v2` object dialect (`{ '>id': 100 }`) is accepted by `TypeCallParamsV3`{lang="ts-type"} for backward compatibility and works with a plain `call`, but not here: cursor pagination appends `[cursorIdKey, '>', nextId]` to the same filter on every page, so an array is the only shape it can extend. Passing an object throws `SdkError`{lang="ts-type"} with code `JSSDK_ACTION_V3_LIST_FILTER_NOT_ARRAY`. Before v2.2.0 it was accepted and then failed mid-walk with `filter is not iterable`. - **Only for list methods**: Intended only for methods that return data arrays. ## Error Handling diff --git a/packages/jssdk/src/core/actions/v3/call-list.ts b/packages/jssdk/src/core/actions/v3/call-list.ts index 0454a25d..0943ea35 100644 --- a/packages/jssdk/src/core/actions/v3/call-list.ts +++ b/packages/jssdk/src/core/actions/v3/call-list.ts @@ -1,12 +1,44 @@ import type { ActionOptions } from '../abstract-action' -import type { TypeCallParams, TypeCallParamsV3 } from '../../../types/http' +import type { TypeCallParams, TypeCallParamsV3, TypeFilterV3 } from '../../../types/http' import { AbstractAction } from '../abstract-action' import { Result } from '../../result' +import { SdkError } from '../../sdk-error' import { keysetPaginate, KeysetPaginationError } from './_keyset-paginate' +/** + * Reject a non-array `filter` with a message that names the fix. + * + * The type above already rules this out, but a JavaScript caller has no types, + * and `TypeCallParamsV3` deliberately still accepts the v2 object dialect — so + * a value that is legal one layer up arrives here illegal. Without this the + * failure is `filter is not iterable`, thrown from a spread inside the paging + * loop, which says nothing about which argument was wrong. + */ +function assertArrayFilter(filter: unknown, action: string): asserts filter is undefined | TypeFilterV3 { + if (filter === undefined || Array.isArray(filter)) { + return + } + + throw new SdkError({ + code: 'JSSDK_ACTION_V3_LIST_FILTER_NOT_ARRAY', + description: `${action}: \`filter\` must be the restApi:v3 array form, e.g. [['id', '>', 100]] or FilterV3.build(...). ` + + `The restApi:v2 object dialect ({ '>id': 100 }) cannot be used here, because keyset pagination extends the filter with a cursor condition.`, + status: 500 + }) +} + export type ActionCallListV3 = ActionOptions & { method: string - params?: Omit + /** + * `filter` is narrowed to the v3 array form here, unlike {@link TypeCallParamsV3}, + * which also accepts the v2 object dialect for backward compatibility. + * + * Keyset pagination is emulated by appending `[cursorIdKey, '>', cursor]` to + * this filter on every page, so an array is not a preference — it is the only + * shape the mechanism can extend. The object form used to be accepted here and + * then threw `filter is not iterable` at runtime, one page into the walk. + */ + params?: Omit & { filter?: TypeFilterV3 } idKey?: string cursorIdKey?: string customKeyForResult: string @@ -88,11 +120,13 @@ export class CallListV3 extends AbstractAction { this._logger.warning('callList.make: user-provided `order` parameter is ignored because cursor-based pagination requires ordering by cursorIdKey. Use `filter` to narrow results instead.').catch(() => {}) } + assertArrayFilter(params['filter'], 'callList.make') + const { order: _ignoredOrder, ...restParams } = params as TypeCallParams const requestParams: TypeCallParams = { ...restParams, order: { [cursorIdKey]: 'ASC' }, - filter: [...(params['filter'] || [])], + filter: [...(params['filter'] ?? [])], pagination: { page: 0, limit: batchSize } } diff --git a/packages/jssdk/src/core/actions/v3/fetch-list.ts b/packages/jssdk/src/core/actions/v3/fetch-list.ts index b6d4c7b1..88c186b5 100644 --- a/packages/jssdk/src/core/actions/v3/fetch-list.ts +++ b/packages/jssdk/src/core/actions/v3/fetch-list.ts @@ -1,12 +1,43 @@ import type { ActionOptions } from '../abstract-action' -import type { TypeCallParams, TypeCallParamsV3 } from '../../../types/http' +import type { TypeCallParams, TypeCallParamsV3, TypeFilterV3 } from '../../../types/http' import { AbstractAction } from '../abstract-action' import { SdkError } from '../../sdk-error' import { keysetPaginate, KeysetPaginationError } from './_keyset-paginate' +/** + * Reject a non-array `filter` with a message that names the fix. + * + * The type above already rules this out, but a JavaScript caller has no types, + * and `TypeCallParamsV3` deliberately still accepts the v2 object dialect — so + * a value that is legal one layer up arrives here illegal. Without this the + * failure is `filter is not iterable`, thrown from a spread inside the paging + * loop, which says nothing about which argument was wrong. + */ +function assertArrayFilter(filter: unknown, action: string): asserts filter is undefined | TypeFilterV3 { + if (filter === undefined || Array.isArray(filter)) { + return + } + + throw new SdkError({ + code: 'JSSDK_ACTION_V3_LIST_FILTER_NOT_ARRAY', + description: `${action}: \`filter\` must be the restApi:v3 array form, e.g. [['id', '>', 100]] or FilterV3.build(...). ` + + `The restApi:v2 object dialect ({ '>id': 100 }) cannot be used here, because keyset pagination extends the filter with a cursor condition.`, + status: 500 + }) +} + export type ActionFetchListV3 = ActionOptions & { method: string - params?: Omit + /** + * `filter` is narrowed to the v3 array form here, unlike {@link TypeCallParamsV3}, + * which also accepts the v2 object dialect for backward compatibility. + * + * Keyset pagination is emulated by appending `[cursorIdKey, '>', cursor]` to + * this filter on every page, so an array is not a preference — it is the only + * shape the mechanism can extend. The object form used to be accepted here and + * then threw `filter is not iterable` at runtime, one page into the walk. + */ + params?: Omit & { filter?: TypeFilterV3 } idKey?: string cursorIdKey?: string customKeyForResult: string @@ -90,11 +121,13 @@ export class FetchListV3 extends AbstractAction { this._logger.warning('fetchList.make: user-provided `order` parameter is ignored because cursor-based pagination requires ordering by cursorIdKey. Use `filter` to narrow results instead.').catch(() => {}) } + assertArrayFilter(params['filter'], 'fetchList.make') + const { order: _ignoredOrder, ...restParams } = params as TypeCallParams const requestParams: TypeCallParams = { ...restParams, order: { [cursorIdKey]: 'ASC' }, - filter: [...(params['filter'] || [])], + filter: [...(params['filter'] ?? [])], pagination: { page: 0, limit: batchSize } } diff --git a/packages/jssdk/src/core/http/v2.ts b/packages/jssdk/src/core/http/v2.ts index 137dcb04..2b7a4a8c 100644 --- a/packages/jssdk/src/core/http/v2.ts +++ b/packages/jssdk/src/core/http/v2.ts @@ -1,7 +1,7 @@ import type { BatchCommandsArrayUniversal, BatchCommandsObjectUniversal, - BatchNamedCommandsUniversal, ICallBatchOptions, ICallBatchResult, + BatchNamedCommandsUniversal, BatchRequestEnvelopeV2, ICallBatchOptions, ICallBatchResult, TypeHttp } from '../../types/http' import type { AuthActions } from '../../types/auth' @@ -83,12 +83,17 @@ export class HttpV2 extends AbstractHttp implements TypeHttp { }) } + // Named rather than inline: this is the batch envelope, not call params — + // see BatchRequestEnvelopeV2. It reaches `call` through a parameter typed + // `TypeCallParams`, which accepts it only because of the index signature. + const envelope: BatchRequestEnvelopeV2 = { + halt: opts.isHaltOnError ? 1 : 0, + cmd: interactionBatch.getCommandsForCall() + } + const responseBatch = await this.call>( 'batch', - { - halt: opts.isHaltOnError ? 1 : 0, - cmd: interactionBatch.getCommandsForCall() - }, + envelope, requestId ) diff --git a/packages/jssdk/src/core/http/v3.ts b/packages/jssdk/src/core/http/v3.ts index 64f285b2..41962d07 100644 --- a/packages/jssdk/src/core/http/v3.ts +++ b/packages/jssdk/src/core/http/v3.ts @@ -88,6 +88,11 @@ export class HttpV3 extends AbstractHttp implements TypeHttp { const responseBatch = await this.call>( 'batch', + // A cast, and an honest one: `restApi:v3` sends the commands AS the request + // body — there is no `{ halt, cmd }` envelope to wrap them in — while + // `call` types its params as `TypeCallParams`. So the request body is not + // call params on this path either, and the cast is what bridges that until + // `call` distinguishes the two. See BatchRequestEnvelopeV2 for the v2 side. interactionBatch.getCommandsForCall() as TypeCallParams, requestId ) diff --git a/packages/jssdk/src/types/http.ts b/packages/jssdk/src/types/http.ts index a06a1182..5c631363 100644 --- a/packages/jssdk/src/types/http.ts +++ b/packages/jssdk/src/types/http.ts @@ -86,6 +86,30 @@ export type TypeCallParamsV3 = Omit & { filter?: TypeFilterV3 | TypeFilterV2 } +/** + * What the transport sends for a `batch` CALL — not call params. + * + * `TypeHttp.call` types its `params` as {@link TypeCallParams}, and the batch + * request rides through it: `{ halt, cmd }` is neither a filter nor a select, + * and it type-checks today only because of the permissive index signature. This + * type names the shape so the intent is visible at the two callsites that build + * it (`core/http/v2.ts`, `core/http/v3.ts`), and so the eventual narrowing of + * that index signature has something to point at. + * + * `cmd` is `unknown` because its shape is mode-specific — a string array or a + * `Record` of `method?query` lines, depending on whether the + * caller used array or named commands. `buildCommands` owns that decision. + * + * `restApi:v3` has no envelope: it sends the commands as the request body, with + * no `halt` (per-command `parallel` replaces it), so there is nothing to name on + * that side — see the comment at its callsite in `core/http/v3.ts`. + */ +export type BatchRequestEnvelopeV2 = { + /** `1` stops the batch at the first failing command, `0` runs them all. */ + halt: 0 | 1 + cmd: unknown +} + // region Batch interface //// /** * Options for batch calls diff --git a/test/integration/core/list-v3-filter-shape.unit.spec.ts b/test/integration/core/list-v3-filter-shape.unit.spec.ts new file mode 100644 index 00000000..d0ee9600 --- /dev/null +++ b/test/integration/core/list-v3-filter-shape.unit.spec.ts @@ -0,0 +1,121 @@ +/** + * #279 — the v3 list actions require the array filter, and say so. + * + * `TypeCallParamsV3.filter` deliberately accepts both the v3 array of triples and + * the v2 object dialect, for backward compatibility. That union is fine for a + * single `call`, which forwards the filter untouched — but NOT for `callList` / + * `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. + * + * Before this, the object form was accepted by the types and then threw + * `filter is not iterable` from a spread one page into the walk — a runtime + * failure on a shape the public type and the docs both promise. The type now + * narrows it away at compile time, and this guard turns the JavaScript caller's + * version of the same mistake into an error that names the fix. + * + * Pure logic, no portal — jsSdk:unit. + */ +import { describe, it, expect } from 'vitest' +import { CallListV3 } from '../../../packages/jssdk/src/core/actions/v3/call-list' +import { FetchListV3 } from '../../../packages/jssdk/src/core/actions/v3/fetch-list' +import { SdkError } from '../../../packages/jssdk/src/core/sdk-error' + +const logger = { + warning: async () => {}, + error: async () => {}, + info: async () => {}, + log: async () => {}, + debug: async () => {}, + trace: async () => {} +} as never + +/** One page of two rows, then an empty page so the walk terminates. */ +function makeB24() { + const seen: unknown[] = [] + let n = 0 + const make = async (options: { params?: { filter?: unknown } }) => { + seen.push(options.params?.filter) + n += 1 + const items = n === 1 ? [{ id: '1' }, { id: '2' }] : [] + return { + isSuccess: true, + getData: () => ({ result: { items } }), + getErrorMessages: () => [], + errors: [] as Array<[string, Error]> + } as never + } + return { b24: { actions: { v3: { call: { make } } } } as never, seen } +} + +const V2_OBJECT_FILTER = { '>id': 100 } as never + +describe('#279 v3 list actions reject the v2 object filter', () => { + it('callList throws a named SdkError instead of "filter is not iterable"', async () => { + const { b24 } = makeB24() + + await expect( + new CallListV3(b24, logger).make({ + method: 'tasks.task.list', + customKeyForResult: 'items', + params: { filter: V2_OBJECT_FILTER } + }) + ).rejects.toThrow(SdkError) + }) + + it('the message names the code and the fix', async () => { + const { b24 } = makeB24() + + await new CallListV3(b24, logger).make({ + method: 'tasks.task.list', + customKeyForResult: 'items', + params: { filter: V2_OBJECT_FILTER } + }).catch((error: unknown) => { + expect((error as SdkError).code).toBe('JSSDK_ACTION_V3_LIST_FILTER_NOT_ARRAY') + // The point of the guard: the old failure said nothing about which + // argument was wrong, or what to write instead. + expect((error as SdkError).message).toContain('FilterV3.build') + expect((error as SdkError).message).not.toContain('not iterable') + }) + }) + + it('fetchList throws the same way — it is generator-based, so it must be drained', async () => { + const { b24 } = makeB24() + + await expect((async () => { + const pages = new FetchListV3(b24, logger).make({ + method: 'tasks.task.list', + customKeyForResult: 'items', + params: { filter: V2_OBJECT_FILTER } + }) + for await (const _page of pages) { /* drained to reach the throw */ } + })()).rejects.toThrow(SdkError) + }) + + it('accepts the array form and extends it with the cursor condition', async () => { + const { b24, seen } = makeB24() + + const result = await new CallListV3(b24, logger).make({ + method: 'tasks.task.list', + customKeyForResult: 'items', + params: { filter: [['stageId', '=', 'NEW']] } + }) + + expect(result.isSuccess).toBe(true) + // The caller's condition survives, and the cursor condition is appended — + // which is exactly what the object form could not support. + expect(seen[0]).toEqual([['stageId', '=', 'NEW'], ['id', '>', 0]]) + }) + + it('accepts no filter at all', async () => { + const { b24, seen } = makeB24() + + const result = await new CallListV3(b24, logger).make({ + method: 'tasks.task.list', + customKeyForResult: 'items' + }) + + expect(result.isSuccess).toBe(true) + expect(seen[0]).toEqual([['id', '>', 0]]) + }) +}) From 84fcf90069128abfad2ce2fb872f4fee47c93898 Mon Sep 17 00:00:00 2001 From: Shevchik Igor Date: Wed, 26 Aug 2026 11:31:40 +0000 Subject: [PATCH 2/3] fix(core): one copy of the filter guard, and a test that could not fail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01F22e2ft66y7nuBJjzdThBr --- .../2.call-list-rest-api-ver3.md | 2 +- .../2.fetch-list-rest-api-ver3.md | 2 +- .../70.core-http.md | 4 ++ .../src/core/actions/v3/_keyset-paginate.ts | 46 ++++++++++++++++++- .../jssdk/src/core/actions/v3/call-list.ts | 25 +--------- .../jssdk/src/core/actions/v3/fetch-list.ts | 24 +--------- packages/jssdk/src/core/sdk-error.ts | 12 +++++ .../core/list-v3-filter-shape.unit.spec.ts | 43 +++++++++++++---- 8 files changed, 98 insertions(+), 60 deletions(-) diff --git a/docs/content/docs/2.working-with-the-rest-api/2.call-list-rest-api-ver3.md b/docs/content/docs/2.working-with-the-rest-api/2.call-list-rest-api-ver3.md index 488717f3..86f57744 100644 --- a/docs/content/docs/2.working-with-the-rest-api/2.call-list-rest-api-ver3.md +++ b/docs/content/docs/2.working-with-the-rest-api/2.call-list-rest-api-ver3.md @@ -118,7 +118,7 @@ Some v3 list methods (e.g. `note.*`) also return a `nextCursor`{lang="ts-type"} - **Page size**: Bitrix24 REST API version 3 limitation — maximum `1000` records per request. - **Sorting is fixed**: The method always sorts by `cursorIdKey` (which defaults to `idKey`) ascending, because cursor pagination relies on `[cursorIdKey, '>', nextId]` filters to walk the dataset. A user-supplied `order` value would break that invariant, so the type signature excludes `order` and any value passed at runtime is stripped with a `warning` log entry. To narrow the result set, use `filter` instead. -- **`filter` must be the v3 array form**: `[['id', '>', 100]]`, or the output of `FilterV3.build(...)`. The `restApi:v2` object dialect (`{ '>id': 100 }`) is accepted by `TypeCallParamsV3`{lang="ts-type"} for backward compatibility and works with a plain `call`, but not here: cursor pagination appends `[cursorIdKey, '>', nextId]` to the same filter on every page, so an array is the only shape it can extend. Passing an object throws `SdkError`{lang="ts-type"} with code `JSSDK_ACTION_V3_LIST_FILTER_NOT_ARRAY`. Before v2.2.0 it was accepted and then failed mid-walk with `filter is not iterable`. +- **`filter` must be the v3 array form**: `[['id', '>', 100]]`, or the output of `FilterV3.build(...)`. The `restApi:v2` object dialect (`{ '>id': 100 }`) is accepted by `TypeCallParamsV3`{lang="ts-type"} for backward compatibility and works with a plain `call`, but not here: cursor pagination appends `[cursorIdKey, '>', nextId]` to the same filter on every page, so an array is the only shape it can extend. Passing an object throws `SdkError`{lang="ts-type"} with code `JSSDK_ACTION_V3_LIST_FILTER_NOT_ARRAY`. It used to be accepted and then failed mid-walk with `filter is not iterable`. - **Only for list methods**: Intended only for methods that return data arrays. ## Error Handling diff --git a/docs/content/docs/2.working-with-the-rest-api/2.fetch-list-rest-api-ver3.md b/docs/content/docs/2.working-with-the-rest-api/2.fetch-list-rest-api-ver3.md index 75048aa7..82acfe78 100644 --- a/docs/content/docs/2.working-with-the-rest-api/2.fetch-list-rest-api-ver3.md +++ b/docs/content/docs/2.working-with-the-rest-api/2.fetch-list-rest-api-ver3.md @@ -127,7 +127,7 @@ Some v3 list methods (e.g. `note.*`) also return a `nextCursor`{lang="ts-type"} - **Page size**: Bitrix24 REST API version 3 limitation — maximum `1000` records per request. - **Sorting is fixed**: The method always sorts by `cursorIdKey` (which defaults to `idKey`) ascending, because cursor pagination relies on `[cursorIdKey, '>', nextId]` filters to walk the dataset. A user-supplied `order` value would break that invariant, so the type signature excludes `order` and any value passed at runtime is stripped with a `warning` log entry. To narrow the result set, use `filter` instead. -- **`filter` must be the v3 array form**: `[['id', '>', 100]]`, or the output of `FilterV3.build(...)`. The `restApi:v2` object dialect (`{ '>id': 100 }`) is accepted by `TypeCallParamsV3`{lang="ts-type"} for backward compatibility and works with a plain `call`, but not here: cursor pagination appends `[cursorIdKey, '>', nextId]` to the same filter on every page, so an array is the only shape it can extend. Passing an object throws `SdkError`{lang="ts-type"} with code `JSSDK_ACTION_V3_LIST_FILTER_NOT_ARRAY`. Before v2.2.0 it was accepted and then failed mid-walk with `filter is not iterable`. +- **`filter` must be the v3 array form**: `[['id', '>', 100]]`, or the output of `FilterV3.build(...)`. The `restApi:v2` object dialect (`{ '>id': 100 }`) is accepted by `TypeCallParamsV3`{lang="ts-type"} for backward compatibility and works with a plain `call`, but not here: cursor pagination appends `[cursorIdKey, '>', nextId]` to the same filter on every page, so an array is the only shape it can extend. Passing an object throws `SdkError`{lang="ts-type"} with code `JSSDK_ACTION_V3_LIST_FILTER_NOT_ARRAY`. It used to be accepted and then failed mid-walk with `filter is not iterable`. - **Only for list methods**: Intended only for methods that return data arrays. ## Error Handling diff --git a/docs/content/docs/2.working-with-the-rest-api/70.core-http.md b/docs/content/docs/2.working-with-the-rest-api/70.core-http.md index 081d8284..e3b3e8f3 100644 --- a/docs/content/docs/2.working-with-the-rest-api/70.core-http.md +++ b/docs/content/docs/2.working-with-the-rest-api/70.core-http.md @@ -75,6 +75,10 @@ Low-level batch call. Three input shapes are accepted: For most use cases call [`actions.v2.batch.make`](/docs/working-with-the-rest-api/batch-rest-api-ver2/) / [`actions.v3.batch.make`](/docs/working-with-the-rest-api/batch-rest-api-ver3/), which handle the response unwrapping and `returnAjaxResult`. +::note +`BatchRequestEnvelopeV2`{lang="ts-type"} is exported alongside these types but is not something you construct. It names what the `restApi:v2` transport puts on the wire for a batch — `{ halt, cmd }` — which reaches `call` through a parameter typed `TypeCallParams`{lang="ts-type"} and type-checks only because of that type's permissive index signature. It is documented here so an exported symbol is not a mystery, not because a caller needs it. `restApi:v3` has no envelope: the commands are the request body. +:: + ## Limiter Configuration ```ts-type diff --git a/packages/jssdk/src/core/actions/v3/_keyset-paginate.ts b/packages/jssdk/src/core/actions/v3/_keyset-paginate.ts index 6e25c2c2..f129d25b 100644 --- a/packages/jssdk/src/core/actions/v3/_keyset-paginate.ts +++ b/packages/jssdk/src/core/actions/v3/_keyset-paginate.ts @@ -1,7 +1,51 @@ import type { TypeB24 } from '../../../types/b24' import type { LoggerInterface } from '../../../types/logger' -import type { TypeCallParams } from '../../../types/http' +import type { TypeCallParams, TypeFilterV3 } from '../../../types/http' import type { AjaxResult } from '../../http/ajax-result' +import { SdkError } from '../../sdk-error' + +/** + * Reject a non-array `filter` for the emulated-keyset list actions. + * + * `TypeCallParamsV3.filter` accepts the v3 array of triples AND the v2 object + * dialect, kept for backward compatibility, and that union is right for a plain + * `call`, which forwards the filter untouched. It is wrong for `callList` / + * `fetchList`: those append `[cursorIdKey, '>', cursor]` to the same filter on + * every page, so an array is not a preference, it is the only shape the + * mechanism can extend. Passing the object form used to throw `filter is not + * iterable` from a spread, one page into the walk. + * + * Both action option types already narrow `filter` to {@link 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 anyone who wrote `params as any`. + * + * `callTail` / `fetchTail` do NOT need this. They paginate through the separate + * `cursor` parameter and forward `filter` untouched, so the object dialect is + * harmless there — which is why the fix stops at two of the four v3 walkers. + * + * @throws {SdkError} `JSSDK_ACTION_V3_LIST_FILTER_NOT_ARRAY` + */ +export function assertArrayFilter( + filter: unknown, + action: string +): asserts filter is undefined | TypeFilterV3 { + if (filter === undefined || Array.isArray(filter)) { + return + } + + // Static text plus the caller-supplied `action` label only. Never interpolate + // the filter, or any other caller value, into an SdkError description: unlike + // AjaxError, SdkError does NOT run its message through + // `redactSensitiveParams`, and a filter legitimately carries user data — the + // email or phone number being searched for. + throw new SdkError({ + code: 'JSSDK_ACTION_V3_LIST_FILTER_NOT_ARRAY', + description: `${action}: \`filter\` must be the restApi:v3 array form, e.g. [['id', '>', 100]] or FilterV3.build(...). ` + + `The restApi:v2 object dialect ({ '>id': 100 }) cannot be used here, because keyset pagination extends the filter with a cursor condition.`, + status: 500 + }) +} /** * Thrown by {@link keysetPaginate} when the underlying v3 `call` reports a soft diff --git a/packages/jssdk/src/core/actions/v3/call-list.ts b/packages/jssdk/src/core/actions/v3/call-list.ts index 0943ea35..11d6fa09 100644 --- a/packages/jssdk/src/core/actions/v3/call-list.ts +++ b/packages/jssdk/src/core/actions/v3/call-list.ts @@ -2,30 +2,7 @@ import type { ActionOptions } from '../abstract-action' import type { TypeCallParams, TypeCallParamsV3, TypeFilterV3 } from '../../../types/http' import { AbstractAction } from '../abstract-action' import { Result } from '../../result' -import { SdkError } from '../../sdk-error' -import { keysetPaginate, KeysetPaginationError } from './_keyset-paginate' - -/** - * Reject a non-array `filter` with a message that names the fix. - * - * The type above already rules this out, but a JavaScript caller has no types, - * and `TypeCallParamsV3` deliberately still accepts the v2 object dialect — so - * a value that is legal one layer up arrives here illegal. Without this the - * failure is `filter is not iterable`, thrown from a spread inside the paging - * loop, which says nothing about which argument was wrong. - */ -function assertArrayFilter(filter: unknown, action: string): asserts filter is undefined | TypeFilterV3 { - if (filter === undefined || Array.isArray(filter)) { - return - } - - throw new SdkError({ - code: 'JSSDK_ACTION_V3_LIST_FILTER_NOT_ARRAY', - description: `${action}: \`filter\` must be the restApi:v3 array form, e.g. [['id', '>', 100]] or FilterV3.build(...). ` - + `The restApi:v2 object dialect ({ '>id': 100 }) cannot be used here, because keyset pagination extends the filter with a cursor condition.`, - status: 500 - }) -} +import { assertArrayFilter, keysetPaginate, KeysetPaginationError } from './_keyset-paginate' export type ActionCallListV3 = ActionOptions & { method: string diff --git a/packages/jssdk/src/core/actions/v3/fetch-list.ts b/packages/jssdk/src/core/actions/v3/fetch-list.ts index 88c186b5..da3a9ff9 100644 --- a/packages/jssdk/src/core/actions/v3/fetch-list.ts +++ b/packages/jssdk/src/core/actions/v3/fetch-list.ts @@ -2,29 +2,7 @@ import type { ActionOptions } from '../abstract-action' import type { TypeCallParams, TypeCallParamsV3, TypeFilterV3 } from '../../../types/http' import { AbstractAction } from '../abstract-action' import { SdkError } from '../../sdk-error' -import { keysetPaginate, KeysetPaginationError } from './_keyset-paginate' - -/** - * Reject a non-array `filter` with a message that names the fix. - * - * The type above already rules this out, but a JavaScript caller has no types, - * and `TypeCallParamsV3` deliberately still accepts the v2 object dialect — so - * a value that is legal one layer up arrives here illegal. Without this the - * failure is `filter is not iterable`, thrown from a spread inside the paging - * loop, which says nothing about which argument was wrong. - */ -function assertArrayFilter(filter: unknown, action: string): asserts filter is undefined | TypeFilterV3 { - if (filter === undefined || Array.isArray(filter)) { - return - } - - throw new SdkError({ - code: 'JSSDK_ACTION_V3_LIST_FILTER_NOT_ARRAY', - description: `${action}: \`filter\` must be the restApi:v3 array form, e.g. [['id', '>', 100]] or FilterV3.build(...). ` - + `The restApi:v2 object dialect ({ '>id': 100 }) cannot be used here, because keyset pagination extends the filter with a cursor condition.`, - status: 500 - }) -} +import { assertArrayFilter, keysetPaginate, KeysetPaginationError } from './_keyset-paginate' export type ActionFetchListV3 = ActionOptions & { method: string diff --git a/packages/jssdk/src/core/sdk-error.ts b/packages/jssdk/src/core/sdk-error.ts index cd60b0d5..24f08da4 100644 --- a/packages/jssdk/src/core/sdk-error.ts +++ b/packages/jssdk/src/core/sdk-error.ts @@ -1,5 +1,17 @@ export type SdkErrorDetails = { code: string + /** + * Human-readable detail. **Never interpolate a caller-supplied value into + * this string** — request params, a filter, a URL, a token. + * + * `AjaxError` runs its `requestInfo` through `redactSensitiveParams` before + * storing it; `SdkError` has no equivalent step, because its description is + * expected to be written by the SDK rather than assembled from input. That + * expectation is the only thing keeping a credential out of it, and error + * messages travel — into logs, into failure reports, into Bitrix24 server-side + * records. A filter alone legitimately carries user data: the email or phone + * number being searched for. + */ description?: string status: number originalError?: unknown diff --git a/test/integration/core/list-v3-filter-shape.unit.spec.ts b/test/integration/core/list-v3-filter-shape.unit.spec.ts index d0ee9600..6b4d886e 100644 --- a/test/integration/core/list-v3-filter-shape.unit.spec.ts +++ b/test/integration/core/list-v3-filter-shape.unit.spec.ts @@ -66,19 +66,42 @@ describe('#279 v3 list actions reject the v2 object filter', () => { it('the message names the code and the fix', async () => { const { b24 } = makeB24() - await new CallListV3(b24, logger).make({ - method: 'tasks.task.list', - customKeyForResult: 'items', - params: { filter: V2_OBJECT_FILTER } - }).catch((error: unknown) => { - expect((error as SdkError).code).toBe('JSSDK_ACTION_V3_LIST_FILTER_NOT_ARRAY') - // The point of the guard: the old failure said nothing about which - // argument was wrong, or what to write instead. - expect((error as SdkError).message).toContain('FilterV3.build') - expect((error as SdkError).message).not.toContain('not iterable') + // `rejects.toMatchObject`, not `.catch(err => expect(...))`. The `.catch` + // form passes VACUOUSLY when the promise resolves — the callback simply + // never runs, no assertion executes, and vitest reports green. Verified: + // with the guard neutered AND the spread made object-tolerant, so the call + // succeeds, the `.catch` version stayed green while the test above went + // red. A test for an error message must fail when there is no error. + await expect( + new CallListV3(b24, logger).make({ + method: 'tasks.task.list', + customKeyForResult: 'items', + params: { filter: V2_OBJECT_FILTER } + }) + ).rejects.toMatchObject({ + code: 'JSSDK_ACTION_V3_LIST_FILTER_NOT_ARRAY', + // The point of the guard: the old failure named neither the argument that + // was wrong nor what to write instead. + message: expect.stringContaining('FilterV3.build') }) }) + it('fetchList reports the same code, not just the same class', async () => { + // Without the guard this path also throws — but a TypeError, not an + // SdkError. Asserting the code as well as the class is what distinguishes + // "guarded" from "crashed in a different way". + const { b24 } = makeB24() + + await expect((async () => { + const pages = new FetchListV3(b24, logger).make({ + method: 'tasks.task.list', + customKeyForResult: 'items', + params: { filter: V2_OBJECT_FILTER } + }) + for await (const _page of pages) { /* drained to reach the throw */ } + })()).rejects.toMatchObject({ code: 'JSSDK_ACTION_V3_LIST_FILTER_NOT_ARRAY' }) + }) + it('fetchList throws the same way — it is generator-based, so it must be drained', async () => { const { b24 } = makeB24() From b13bd18df9f655e0210e8ad6c1116dd1abfc1cbc Mon Sep 17 00:00:00 2001 From: Shevchik Igor Date: Wed, 26 Aug 2026 11:51:37 +0000 Subject: [PATCH 3/3] docs: register the new error code, and the stamps my own change invalidated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01F22e2ft66y7nuBJjzdThBr --- .../2.working-with-the-rest-api/1.call-rest-api-ver2.md | 2 +- .../2.working-with-the-rest-api/1.call-rest-api-ver3.md | 2 +- .../2.fetch-list-rest-api-ver2.md | 2 +- .../2.working-with-the-rest-api/3.batch-rest-api-ver2.md | 2 +- .../2.working-with-the-rest-api/3.batch-rest-api-ver3.md | 2 +- .../4.batch-by-chunk-rest-api-ver2.md | 2 +- .../4.batch-by-chunk-rest-api-ver3.md | 2 +- docs/content/docs/2.working-with-the-rest-api/6.errors.md | 7 ++++++- docs/content/docs/99.examples/3.webhook-cli-node.md | 2 +- 9 files changed, 14 insertions(+), 9 deletions(-) diff --git a/docs/content/docs/2.working-with-the-rest-api/1.call-rest-api-ver2.md b/docs/content/docs/2.working-with-the-rest-api/1.call-rest-api-ver2.md index 3e854130..f3b0e4bc 100644 --- a/docs/content/docs/2.working-with-the-rest-api/1.call-rest-api-ver2.md +++ b/docs/content/docs/2.working-with-the-rest-api/1.call-rest-api-ver2.md @@ -2,7 +2,7 @@ title: CallV2.make description: 'A method for making Bitrix24 REST API version 2 calls.' category: 'actions' -audited: 2026-07-02 +audited: 2026-08-26 restApiVersion: 'rest-api-ver2' navigation.title: Call links: diff --git a/docs/content/docs/2.working-with-the-rest-api/1.call-rest-api-ver3.md b/docs/content/docs/2.working-with-the-rest-api/1.call-rest-api-ver3.md index 6eb7a268..9fa0320f 100644 --- a/docs/content/docs/2.working-with-the-rest-api/1.call-rest-api-ver3.md +++ b/docs/content/docs/2.working-with-the-rest-api/1.call-rest-api-ver3.md @@ -2,7 +2,7 @@ title: CallV3.make description: 'Method for making Bitrix24 REST API version 3 calls.' category: 'actions' -audited: 2026-07-02 +audited: 2026-08-26 restApiVersion: 'rest-api-ver3' navigation.title: Call links: diff --git a/docs/content/docs/2.working-with-the-rest-api/2.fetch-list-rest-api-ver2.md b/docs/content/docs/2.working-with-the-rest-api/2.fetch-list-rest-api-ver2.md index 94b7741e..293e3c96 100644 --- a/docs/content/docs/2.working-with-the-rest-api/2.fetch-list-rest-api-ver2.md +++ b/docs/content/docs/2.working-with-the-rest-api/2.fetch-list-rest-api-ver2.md @@ -2,7 +2,7 @@ title: FetchListV2.make description: 'Returns an AsyncGenerator that allows processing data from list methods of Bitrix24 REST API version 2 as it is received without loading the entire array into memory at once. This is especially useful when working with very large volumes of data.' category: 'actions' -audited: 2026-08-18 +audited: 2026-08-26 restApiVersion: 'rest-api-ver2' navigation: title: FetchList diff --git a/docs/content/docs/2.working-with-the-rest-api/3.batch-rest-api-ver2.md b/docs/content/docs/2.working-with-the-rest-api/3.batch-rest-api-ver2.md index 6c912261..5c2e38d0 100644 --- a/docs/content/docs/2.working-with-the-rest-api/3.batch-rest-api-ver2.md +++ b/docs/content/docs/2.working-with-the-rest-api/3.batch-rest-api-ver2.md @@ -2,7 +2,7 @@ title: BatchV2.make description: 'Method for executing batch requests to Bitrix24 REST API version 2. Allows executing up to 50 commands in a single API call.' category: 'actions' -audited: 2026-07-02 +audited: 2026-08-26 restApiVersion: 'rest-api-ver2' navigation.title: Batch links: diff --git a/docs/content/docs/2.working-with-the-rest-api/3.batch-rest-api-ver3.md b/docs/content/docs/2.working-with-the-rest-api/3.batch-rest-api-ver3.md index 108e6b9f..249df432 100644 --- a/docs/content/docs/2.working-with-the-rest-api/3.batch-rest-api-ver3.md +++ b/docs/content/docs/2.working-with-the-rest-api/3.batch-rest-api-ver3.md @@ -2,7 +2,7 @@ title: BatchV3.make description: 'Method for executing batch requests to Bitrix24 REST API version 3. Allows executing up to 50 commands in a single API call.' category: 'actions' -audited: 2026-07-02 +audited: 2026-08-26 restApiVersion: 'rest-api-ver3' navigation.title: Batch links: diff --git a/docs/content/docs/2.working-with-the-rest-api/4.batch-by-chunk-rest-api-ver2.md b/docs/content/docs/2.working-with-the-rest-api/4.batch-by-chunk-rest-api-ver2.md index fec1f916..2d1f8cb8 100644 --- a/docs/content/docs/2.working-with-the-rest-api/4.batch-by-chunk-rest-api-ver2.md +++ b/docs/content/docs/2.working-with-the-rest-api/4.batch-by-chunk-rest-api-ver2.md @@ -2,7 +2,7 @@ title: BatchByChunkV2.make description: 'Method for executing batch requests with automatic chunking for any number of commands. Automatically splits large command sets into batches of 50 and executes them sequentially. Use only arrays of tuples or arrays of objects.' category: 'actions' -audited: 2026-07-02 +audited: 2026-08-26 restApiVersion: 'rest-api-ver2' navigation: title: BatchByChunk diff --git a/docs/content/docs/2.working-with-the-rest-api/4.batch-by-chunk-rest-api-ver3.md b/docs/content/docs/2.working-with-the-rest-api/4.batch-by-chunk-rest-api-ver3.md index 2cbff8da..20b73718 100644 --- a/docs/content/docs/2.working-with-the-rest-api/4.batch-by-chunk-rest-api-ver3.md +++ b/docs/content/docs/2.working-with-the-rest-api/4.batch-by-chunk-rest-api-ver3.md @@ -2,7 +2,7 @@ title: BatchByChunkV3.make description: 'Method for executing batch requests to Bitrix24 REST API version 3 with automatic chunking for any number of commands. Automatically splits large command sets into batches of 50 and executes them sequentially. Use only arrays of tuples or arrays of objects.' category: 'actions' -audited: 2026-07-02 +audited: 2026-08-26 restApiVersion: 'rest-api-ver3' navigation: title: BatchByChunk diff --git a/docs/content/docs/2.working-with-the-rest-api/6.errors.md b/docs/content/docs/2.working-with-the-rest-api/6.errors.md index 7ff3deaa..10b37b14 100644 --- a/docs/content/docs/2.working-with-the-rest-api/6.errors.md +++ b/docs/content/docs/2.working-with-the-rest-api/6.errors.md @@ -3,7 +3,7 @@ title: Error codes and handling description: 'Reference for SdkError and AjaxError codes raised by the SDK, plus the Bitrix24 REST error codes that surface through them.' navigation: title: Errors -audited: 2026-07-03 +audited: 2026-08-26 links: - label: SdkError iconName: GitHubIcon @@ -24,6 +24,10 @@ links: The SDK raises errors through two related classes: - `SdkError` — thrown by SDK code itself (validation, configuration, deprecated paths, internal invariants). Always carries a `code`, a `status` (HTTP-like), and an optional `originalError`. Since #189 `originalError` is **non-enumerable**: it stays readable as `err.originalError` for local debugging, but a spread `{ ...err }`, `Object.keys(err)`, `JSON.stringify(err)`, or a Sentry-style capture skips it — so the raw transport error (which may carry a webhook secret in its `config`) can't leak through generic serialization. Prefer `code` / `status` / `message` for anything you log. +::caution +**`SdkError`'s `description` is not redacted.** `AjaxError` runs its `requestInfo` through `redactSensitiveParams`; `SdkError` has no equivalent step, because its description is expected to be written by the SDK rather than assembled from input. If you construct an `SdkError` yourself, do not interpolate request params, a filter, a URL or a token into it — a filter alone legitimately carries user data, such as the email or phone number being searched for, and error messages travel into logs and failure reports. +:: + - `AjaxError extends SdkError` — thrown when an HTTP call to Bitrix24 fails. Adds `requestInfo` (`method`, `requestId`, request params) so you can correlate with portal-side logs. Since v1.1.2 (#39), `requestInfo` does **not** include the full request URL and credential-bearing fields inside `params` are redacted — the goal is to keep webhook secrets out of `toJSON()` / `toString()` output. Method-style results that don't throw — `Call`, `CallList`, `Batch`, `BatchByChunk` — surface failures through `Result`/`AjaxResult`: check `.isSuccess` and read `.getErrorMessages()`. `FetchList`, by contrast, **does** throw on failure (the generator can't complete partially). @@ -77,6 +81,7 @@ Codes are stable strings — match on them, don't parse messages. | `JSSDK_INTERACTION_BATCH_STRATEGY_V2_EMPTY_COMMANDS` | 400 | v2 batch processing | Same, for v2. | | `JSSDK_INTERACTION_BATCH_STRATEGY_V2_EMPTY_COMMAND_RESPONSE` | 500 | v2 batch processing | A command in the response had no body — usually portal-side. | | `JSSDK_INTERACTION_BATCH_EMPTY_PROCESSING_STRATEGY` | 500 | batch processing | Internal — strategy lookup failed. | +| `JSSDK_ACTION_V3_LIST_FILTER_NOT_ARRAY` | 500 | `actions.v3.callList` / `fetchList` | `filter` was the `restApi:v2` object dialect (`{ '>id': 100 }`). These actions emulate keyset pagination by appending `[cursorIdKey, '>', cursor]` to the filter, so only the v3 array form can be extended. Use `[['id', '>', 100]]` or `FilterV3.build(...)`. `callTail` / `fetchTail` are unaffected — they paginate through `cursor` and forward `filter` untouched. | | `JSSDK_INTERACTION_BATCH_ROW_FAIL` | 500 | batch row parser | A single batch row could not be parsed. | | `JSSDK_INVALID_PARAMS` | 400 | HTTP transport | The shape of `params` was rejected before the request was sent. | | `JSSDK_PARAMS_TOO_LARGE` | 413 | HTTP transport | Serialized request body exceeded the size limit. Split the call. | diff --git a/docs/content/docs/99.examples/3.webhook-cli-node.md b/docs/content/docs/99.examples/3.webhook-cli-node.md index 4900cabf..d3987e80 100644 --- a/docs/content/docs/99.examples/3.webhook-cli-node.md +++ b/docs/content/docs/99.examples/3.webhook-cli-node.md @@ -1,7 +1,7 @@ --- title: 'Recipe: Webhook CLI smoke test' description: 'A 30-line Node script that authenticates against a Bitrix24 portal via inbound webhook and prints the calling user — useful as the first thing you run after creating a webhook.' -audited: 2026-08-24 +audited: 2026-08-26 category: 'examples' featured: true cookbookOrder: 1