nautechsystems/nautilus_trader · error · anyhow::Error
AX fills returned an empty next_cursor
Error message
AX fills returned an empty next_cursor
What it means
The /fills response carried a next_cursor that is present but an empty string. The client cannot distinguish an empty cursor from a real continuation token, and treating it as either end-of-chain or a fetch key risks skipping data or looping, so it fails fast. A legal end of the chain is next_cursor: null/absent.
Source
Thrown at crates/adapters/architect_ax/src/http/client.rs:2290
anyhow::ensure!(
seen_trade_ids.insert(fill.trade_id.clone()),
"AX fills pagination returned duplicate trade ID {}",
fill.trade_id
);
fills.push(fill);
}
if let Some(total_count) = expected_total {
anyhow::ensure!(
fills.len() as i64 <= total_count,
"AX fills pagination returned more unique rows ({}) than total_count {total_count}",
fills.len()
);
}
match response.next_cursor {
Some(next_cursor) => {
anyhow::ensure!(
!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 {View on GitHub (pinned to a4b06ed870)
Solutions
- Reproduce with curl: call GET /fills with the failing cursor and inspect the raw next_cursor field
- Check the AX API changelog for cursor serialization changes between your client and server versions
- If AX legitimately emits "" as end-of-chain, fix the response deserialization upstream (map empty string to None) via a PR — keep the ensure! intact
- Retry once to rule out transient proxy corruption
Example fix
// before (stub server)
{"fills": [], "next_cursor": ""}
// after
{"fills": [], "next_cursor": null} Defensive patterns
Strategy: try-catch
Try / catch
match client.request_fill_reports(account_id, start, end).await {
Ok(reports) => Ok(reports),
Err(e) if e.to_string().contains("empty next_cursor") => {
log::error!("AX sent an empty-string cursor; possible API serialization change: {e}");
Err(e) // do not retry: deterministic server serialization behavior
}
Err(e) => Err(e),
} Prevention
- Contract-test the /fills response shape (cursor null vs empty) in CI against your AX environment
- Run a smoke reconciliation after any AX API or adapter upgrade before enabling live tasks
- Keep raw response logging available in staging to confirm cursor serialization changes quickly
When it happens
Trigger: AX serializes the end-of-chain or unknown cursor as "" instead of null in some edge case (empty result set, filter matching nothing, boundary page); an API version change to cursor serialization; a hand-rolled mock returning empty strings.
Common situations: Querying a window with zero fills where the server still emits a cursor field; upgrading the AX API or the adapter in mismatched pairs; test harnesses with default-initialized string fields.
Related errors
- AX fills total_count must be non-negative, was {total_count}
- AX fills returned an empty page with a next_cursor
- AX fills pagination repeated cursor {next_cursor:?}
- AX fills total_count changed during pagination: expected {ex
- AX fills pagination returned duplicate trade ID {}
AI-assisted analysis of nautechsystems/nautilus_trader@a4b06ed870 (2026-08-16).
Data as JSON: /api/errors/e64b635c0e16326f.
Report an issue: GitHub.