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

  1. Reproduce with curl: call GET /fills with the failing cursor and inspect the raw next_cursor field
  2. Check the AX API changelog for cursor serialization changes between your client and server versions
  3. 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
  4. 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

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


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