Files
zhangzhongbo.1105 e1a2c15b8b fix(client): surface later-page failures in --page-all runs
--page-all stopped paginating when a page after the first failed, but
returned the pages it already had as a complete, successful result: exit
code 0 and an ok:true envelope, with the merged result reporting
has_more:false so the output asserted completeness over missing data.

The loop recognised only two failure signals -- a transport error, or a
JSON object whose code read as a non-zero float -- and treated everything
else as a normal page. A gateway 5xx carrying no code was accumulated as
data. A page whose code could not be read at all (absent, null, a string,
0.5, 1e-324) read as 0 through the float, so the loop accepted it as a
continuation, or paginated from it and re-issued the request for its
cursor; the generic api command is not restricted to GET, so that replayed
a POST or DELETE. A body declaring a non-JSON Content-Type was parsed
anyway, and a body with content after its first JSON value was accepted as
that value, cursor included.

Three decisions, each owned once:

Whether a page failed is decided exactly as HandleResponse decides it for
a plain api response, in the same order: CheckResponse first, so a
readable business code wins over the HTTP status, then a status of 400 or
more. A business or HTTP failure therefore classifies the same way whether
or not --page-all is set. Page 1 keeps its output contract and is handed
back whole with a nil error for the command layer to classify and dump; a
later page fails the run.

Whether a page is a step in the pagination is decided by
pageDeclaresSuccess: its business code must be exactly 0, read as a JSON
number whose every mantissa digit is 0. The rule is asymmetric by page
position on purpose. A later page that did not declare success fails the
run before it is emitted or accumulated: the page before it promised more
data, and accepting this one, with or without a cursor or data of its own,
would end the run as complete over that promise. A first page that did
not declare success but carries a continuation token is refused before
output with a non-zero exit, so the streaming formats, which have no
envelope to carry has_more, get a machine-readable signal. A first page
that did not declare success and carries no continuation token is output
on the existing contract: there is nothing to paginate, and plain api
would have shown the same response.

Whether a body is JSON at all is decided by one rule, isJSONBody, shared
with HandleResponse: a declared non-JSON Content-Type is never parsed. On a
4xx/5xx it is classified by status; on a 2xx it is not a page, because
plain api treats it as a download and a paginated run cannot.
ParseJSONResponse now accepts exactly one JSON value, so trailing garbage
or a second value fails the parse on every path that uses it.

callPage keeps the raw response so the loop can read the status. The
identity used for classification is resolved inside the loop, StreamPages
reports how much a failed run had already emitted, and the
PaginationOptions field comments are corrected.

The rule is tested as a decision table -- code shape x data variant
(cursor, terminal list, no data) x HTTP status x first/later page x
PaginateAll/StreamPages -- with every cell's expectation derived from
CheckResponse's own verdict, the status, the page position and the
variant, asserting request count, callback count, category and error.code.
A parity test checks the same shapes, plus unparseable and trailing-content
bodies, against HandleResponse. The command layer pins stdout for each
format.

Left to a follow-up: CheckResponse and errclass still read the code through
float64, so 230027.5 classifies as 230027 and 9007199254740993.0 reports
9007199254740992. Plain api and --page-all now behave identically there;
an exact decode changes every command's classification and needs its own
compatibility review.

Closes #2477
2026-09-03 17:58:39 +08:00
..
2026-06-17 16:29:33 +08:00