Skip to content

fix(utils): keep batch rows when the SDK flags the envelope for a row error - #262

Open
aa-blinov wants to merge 1 commit into
bitrix24:mainfrom
aa-blinov:fix/batch-partial-failures
Open

aa-blinov wants to merge 1 commit into
bitrix24:mainfrom
aa-blinov:fix/batch-partial-failures

Conversation

@aa-blinov

Copy link
Copy Markdown

Summary

A batch call with one bad id returned a single opaque error and no rows, hiding the fact that the other ids had already been applied. batchV2 / batchV3 were checking the batch envelope's isSuccess first, and the SDK marks that envelope failed for per-row errors while filling the rows anyway. They now return the rows whenever rows came back — which is what their own JSDoc already promised.

Type of change

  • feat
  • fix
  • docs
  • chore
  • test
  • refactor
  • ci

Linked issue

Checklist

  • PR title follows Conventional Commits
  • pnpm lint passes
  • pnpm typecheck passes
  • pnpm test passes
  • New or changed code has tests
  • Public-facing changes reflected in docs/ and skills/ (the batch section of skills/manage-bx24-template-mcp/adding-tools.md now warns against re-checking the envelope inside a tool)
  • No unrelated changes
  • No secrets in code, tests, or CI logs

Screenshots / logs

Before, against a live portal — two real tasks plus one nonexistent id:

b24_task_pause { taskId: [4193, 4191, 99999999] }
→ ERR  Действие над задачей не разрешено

Both real tasks were paused by that call. Nothing in the response says so.

After:

b24_task_start { taskId: [4193, 4191, 99999999] }
→ {"batch":true,"verb":"started","total":3,"ok":2,"failed":1,
   "results":[{"taskId":4193,"ok":true,"status":"3"},
              {"taskId":4191,"ok":true,"status":"3"},
              {"taskId":99999999,"ok":false,"error":"Действие над задачей не разрешено"}]}

Notes for reviewers

The mechanism, since it decides whether the fix is right rather than merely convenient:

AbstractBatch._createBatchArrayResult (SDK 1.3.0) builds the row array and then calls _addBatchErrorsIfAny, which copies each per-row error onto the envelope Result. Result.isSuccess is this._errors.size === 0. So under returnAjaxResult: true — which both helpers pass — the envelope is failed whenever any row failed, with every row still populated in getData(). Checking the envelope before reading rows therefore throws away exactly the partial-failure detail the batch tools are built to report, and isHaltOnError: false guarantees the other calls really did execute.

New contract: rows present → return them; no rows → throw. Transport throws are untouched, and per-row reporting through mapBatchRows is unchanged, so no tool needed editing.

One thing worth flagging honestly: the existing test does not throw for per-row failures (isHaltOnError: false semantics) mocked isSuccess: true alongside a failed row — a shape the SDK cannot produce. It passed throughout, which is why the bug survived. I left it in place (it still documents the intent) and added cases that pin the real shape, including the no-rows path that must keep throwing.

This affects every batch tool — the seven lifecycle verbs, checklist complete / renew / delete, rate, dependency add / remove, elapsed-time delete — so it's worth a close look at whether you'd rather have the envelope error also surfaced somewhere in the summary (e.g. a top-level envelopeError field) rather than only per row. I kept the payload shape unchanged on the grounds that the row errors already carry the same text.

… error

Every batch tool promises a per-row envelope — `{ batch, total, ok,
failed, results }` — so the operator can see which ids went through. One
failing row broke that promise: the tool returned a single opaque error
and no rows.

Live repro: `b24_task_pause { taskId: [4193, 4191, 99999999] }` answered
`Действие над задачей не разрешено`, while both real tasks HAD in fact
been paused. The operator has no way to tell, and re-running is the
natural next move.

The cause is the SDK contract, not the portal. With
`returnAjaxResult: true`, `AbstractBatch._createBatchArrayResult` builds
the row array and `_addBatchErrorsIfAny` then copies every per-row error
onto the envelope; `Result.isSuccess` is `errors.size === 0`. So one bad
row out of fifty marks the envelope failed while the other forty-nine
rows sit fully populated in `getData()`. `batchV2` / `batchV3` checked
the envelope first and threw, discarding them.

They now hand back the rows whenever rows came back, and throw only when
nothing did — transport failure, rejected envelope, malformed response.
That is what their own JSDoc already promised ("per-row failures do NOT
throw — they land in the returned array"); the implementation just
didn't. Per-row reporting via `mapBatchRows` is unchanged.

Live re-check after the fix: `b24_task_start { taskId: [4193, 4191,
99999999] }` → `ok: 2, failed: 1`, with the bad id named in its row.

The existing unit test meant to cover this mocked an envelope shape the
SDK never produces — `isSuccess: true` next to a failed row — so the gap
was invisible. The new cases pin the real shape for both helpers, plus
the no-rows path that must still throw. The batch section of
`skills/manage-bx24-template-mcp/adding-tools.md` now warns against
treating the envelope as authoritative inside a tool.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant