nautechsystems/nautilus_trader · error · anyhow::Error

AX fills pagination returned {} unique rows, expected {total

Error message

AX fills pagination returned {} unique rows, expected {total_count}

What it means

After the cursor chain terminates (next_cursor None), the client requires the total number of unique fills collected to equal the pinned total_count exactly. Fewer rows than promised means pages ended early, fills were removed mid-traversal, or the server's counter describes a different row set than the pages serve.

Source

Thrown at crates/adapters/architect_ax/src/http/client.rs:2309

                        !next_cursor.is_empty(),
                        "AX fills returned an empty next_cursor"
                    );
                    anyhow::ensure!(
                        page_len > 0,
                        "AX fills returned an empty page with a next_cursor"
                    );
                    anyhow::ensure!(
                        seen_cursors.insert(next_cursor.clone()),
                        "AX fills pagination repeated cursor {next_cursor:?}"
                    );
                    params.cursor = Some(next_cursor);
                }
                None => break,
            }
        }

        if let Some(total_count) = expected_total {
            anyhow::ensure!(
                fills.len() as i64 == total_count,
                "AX fills pagination returned {} unique rows, expected {total_count}",
                fills.len()
            );
        }

        let ts_init = self.generate_ts_init();
        let mut reports = Vec::with_capacity(fills.len());

        for fill in &fills {
            let instrument = self.resolve_report_instrument(fill.symbol).await?;
            let report = parse_fill_report(fill, account_id, &instrument, ts_init)
                .with_context(|| format!("Failed to parse AX fill {}", fill.trade_id))?;
            reports.push(report);
        }

        Ok(reports)
    }

View on GitHub (pinned to a4b06ed870)

Solutions

  1. Retry with a fully historical, bounded window (fixed start/end in the past) that cannot gain or lose rows
  2. Verify the window semantics: start inclusive vs exclusive relative to the server's counting, and both bounds within the 7-day cap
  3. Cross-check the same window in the AX UI or an export to see whether the count or the rows are wrong
  4. If the mismatch is deterministic on a frozen window, capture all pages with curl and report to AX — count and rows disagree server-side
Defensive patterns

Strategy: retry

Try / catch

for attempt in 0..3 {
    match client.request_fill_reports(account_id, Some(start), Some(end)).await {
        Ok(reports) => return Ok(reports),
        Err(e) if e.to_string().contains("unique rows, expected") => {
            log::warn!("fills row/total mismatch (attempt {}): {e}", attempt + 1);
            tokio::time::sleep(Duration::from_secs(2u64.pow(attempt))).await;
        }
        Err(e) => return Err(e.into()),
    }
}
bail!("fills reconciliation could not obtain a consistent snapshot");

Prevention

When it happens

Trigger: Fills are corrected/restated (removed) during the traversal; the server's total_count is computed with different filter semantics (e.g. no time window) than the paged rows; the chain terminates prematurely due to a boundary bug; end=None races with the server clock.

Common situations: Historical reconciliation jobs on accounts with active corrections; API version drift changing what total_count counts; comparing adapter output against AX's own UI totals and hitting off-by-N rows on window edges.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@a4b06ed870 (2026-08-16). Data as JSON: /api/errors/eb56501c9fc1bd52. Report an issue: GitHub.