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
- Retry with a fully historical, bounded window (fixed start/end in the past) that cannot gain or lose rows
- Verify the window semantics: start inclusive vs exclusive relative to the server's counting, and both bounds within the 7-day cap
- Cross-check the same window in the AX UI or an export to see whether the count or the rows are wrong
- 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
- Use closed historical windows that cannot gain or lose rows during traversal
- Cross-check adapter fill counts against AX's own totals during onboarding so semantic differences surface early
- Fail reconciliation loudly — proceeding with a partial fill set silently corrupts PnL and position state
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
- AX fills total_count must be non-negative, was {total_count}
- AX fills total_count changed during pagination: expected {ex
- AX fills pagination returned duplicate trade ID {}
- AX fills pagination returned more unique rows ({}) than tota
- AX fills returned an empty next_cursor
AI-assisted analysis of nautechsystems/nautilus_trader@a4b06ed870 (2026-08-16).
Data as JSON: /api/errors/eb56501c9fc1bd52.
Report an issue: GitHub.