GitoxideLabs/gitoxide · error

at least in theory

Error message

at least in theory

What it means

`poll_fill_buf` (the async BufRead implementation for sideband-multiplexed packetline streams) asserts that the reader is never asked to fill its buffer while the internal state is `ReadLine` — i.e. while a line-based read is in progress. Hitting the panic means the stream's state machine was interleaved incorrectly, typically by mixing line reads and buffered reads on the same connection.

Solutions

  1. Complete each line read before issuing buffered reads (or vice versa); don't interleave read_line with fill_buf on the same stream
  2. Use the blocking/std implementation or the appropriate mode consistently for the protocol phase (line-based handshake vs binary sideband data)
  3. Serialize access to the stream (own it in one task, or use a mutex) and report the panic to gix-packetline if it occurs with sequential reads

Example fix

// before
let line = reader.read_line(&mut buf).await?; // leaves state ReadLine
let chunk = reader.fill_buf().await?;          // panics
// after
let line = reader.read_line(&mut buf).await?;
reader.consume(line);
let chunk = reader.fill_buf().await?;
Defensive patterns

Strategy: type-guard

Validate before calling

// only call fill_buf when no line read is in progress
debug_assert!(!matches!(stream.state(), ReadLine), "cannot fill_buf during ReadLine state");

Type guard

fn can_fill_buf<S: ReadMode>(s: &S) -> bool { matches!(s.state(), State::Idle { .. }) }

Try / catch

// panic occurs in poll; guard the call site instead
if stream_is_idle(&stream) {
    let buf = Pin::new(&mut stream).fill_buf().await?;
} else {
    return Err(io::Error::new(io::ErrorKind::InvalidInput, "read in progress"));
}

Prevention

When it happens

Trigger: Calling `fill_buf`/BufRead APIs while a `read_line`-style operation is mid-flight on the same pinned stream — e.g. polling fill_buf inside a ReadLine state without completing the line read, or concurrent/mixed use of line and buffered read modes on one packetline sideband.

Common situations: Hand-rolled protocol code mixing `read_line` and `fill_buf`/`read_until` on the same sideband stream; polling a future that owns the reader while another read is pending; spawning tasks sharing one connection without synchronization.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of GitoxideLabs/gitoxide@e73179060b (2026-09-08). Data as JSON: /api/errors/93f774a673074e6c. Report an issue: GitHub.

Appendix: source

Thrown at gix-packetline/src/async_io/sidebands.rs:329

                                        None => {
                                            return Poll::Ready(Err(io::Error::other(
                                                "encountered non-data line in a data-line only context",
                                            )));
                                        }
                                    };
                                }
                            }
                        }
                    }
                };
                this.cap = cap + ofs;
                this.pos = ofs;
            }
        }
        let range = self.pos..self.cap;
        match &self.get_mut().state {
            State::Idle { parent } => Poll::Ready(Ok(&parent.as_ref().expect("parent always available").buf[range])),
            State::ReadLine { .. } => unreachable!("at least in theory"),
        }
    }

    fn consume(self: Pin<&mut Self>, amt: usize) {
        let this = self.get_mut();
        this.pos = std::cmp::min(this.pos + amt, this.cap);
    }
}

impl<T, F> AsyncRead for WithSidebands<'_, T, F>
where
    T: AsyncRead + Unpin,
    F: FnMut(bool, &[u8]) -> ProgressAction + Unpin,
{
    fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8]) -> Poll<std::io::Result<usize>> {
        use std::io::Read;
        let mut rem = ready!(self.as_mut().poll_fill_buf(cx))?;
        let nread = rem.read(buf)?;

View on GitHub (pinned to e73179060b)