nautechsystems/nautilus_trader · error · anyhow::Error

Failed to write COPY trailer: {e}

Error message

Failed to write COPY trailer: {e}

What it means

write_copy_trailer sends the 2-byte (-1 i16) binary COPY trailer that terminates every binary COPY stream. If the sink rejects the trailer, the COPY cannot be finalized and the server will discard the stream; this error wraps that send failure. It is shared by all copy_* functions (blocks, tokens, pools, swaps, liquidity updates, collects).

Source

Thrown at crates/adapters/blockchain/src/cache/copy.rs:918

            .send(row_data)
            .await
            .map_err(|e| anyhow::anyhow!("Failed to write pool data: {e}"))?;
        Ok(())
    }

    /// Writes the PostgreSQL COPY binary format trailer.
    ///
    /// The trailer is a 2-byte value of -1 to indicate end of data.
    async fn write_copy_trailer(
        &self,
        copy_in: &mut sqlx::postgres::PgCopyIn<sqlx::pool::PoolConnection<sqlx::Postgres>>,
    ) -> anyhow::Result<()> {
        // Binary trailer: -1 as i16 to indicate end of data
        let trailer = (-1i16).to_be_bytes();
        copy_in
            .send(trailer.to_vec())
            .await
            .map_err(|e| anyhow::anyhow!("Failed to write COPY trailer: {e}"))?;
        Ok(())
    }
}

fn write_copy_numeric(row: &mut Vec<u8>, value: impl Display) {
    let value = value.to_string();
    row.extend_from_slice(&(value.len() as i32).to_be_bytes());
    row.extend_from_slice(value.as_bytes());
}

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Investigate the earlier rows/errors first — the trailer failure is nearly always a secondary symptom
  2. Check connection liveness and TCP keepalive settings for long COPY operations
  3. Retry the full copy_* operation; partial COPY data is not committed when the trailer fails
  4. Verify no intermediate layer (pool, pgbouncer) is terminating the session during COPY

Example fix

// before: treating trailer failure as root cause
match write_copy_trailer(&mut copy_in).await {
    Err(e) => log::error!("trailer failed: {e}"), // hides real cause
    ...
}
// after: surface the full error chain for diagnosis
write_copy_trailer(&mut copy_in).await
    .map_err(|e| e.context("COPY trailer failed; check prior row errors and connection health"))?;
Defensive patterns

Strategy: try-catch

Validate before calling

// health-check the connection before starting a COPY sequence
sqlx::raw_sql("SELECT 1").fetch_one(&pool).await?;

Try / catch

async fn copy_with_trailer_guard<F, Fut>(op: F) -> anyhow::Result<()>
where F: Fn() -> Fut, Fut: Future<Output = anyhow::Result<()>> {
    op().await.map_err(|e|
        // trailer failures are usually secondary; surface the full chain
        anyhow::anyhow!("COPY sequence failed (check row errors/connection): {e:#}")
    )
}

Prevention

When it happens

Trigger: copy_in.send(trailer) fails at the end of any copy_* operation — almost always because the connection or COPY stream already broke (rows earlier failed, connection dropped, server aborted), leaving no valid channel for the trailer.

Common situations: A mid-stream row error silently closed the sink so the trailer fails; idle timeout triggered between last row and trailer; database restarted during a large backfill.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/c86732f464ae3274. Report an issue: GitHub.