astrid-runtime/astrid · warning

named-pipe client disconnected before transport authenticati

Error message

named-pipe client disconnected before transport authentication

What it means

On Windows, if a named-pipe client closes (or the pipe breaks) before completing transport-level authentication, Astrid surfaces an io::ErrorKind::UnexpectedEof error via pre_authentication_eof(), optionally chaining the underlying OS error in the message. This distinguishes an early, unauthenticated disconnect from post-auth protocol errors; is_pre_authentication_disconnect classifies ERROR_BROKEN_PIPE / ERROR_NO_DATA / ERROR_PIPE_NOT_CONNECTED as this case.

Source

Thrown at crates/astrid-core/src/local_transport/windows.rs:312

    };
    require_current_user_effective_client(&stream)?;
    // Effective-token impersonation above is the authorization boundary.
    // The process-token check is independent defense in depth and pins the
    // client process object while re-reading the pipe-reported PID.
    let peer = require_current_user_process_peer(&stream)?;
    validate_pipe_security(stream_handle(&stream)?)?;
    peer.ensure_still_peer(&stream)?;
    Ok(stream)
}

fn pre_authentication_eof(source: Option<&io::Error>) -> io::Error {
    let message = match source {
        Some(source) => {
            format!("named-pipe client disconnected before transport authentication: {source}")
        },
        None => "named-pipe client disconnected before transport authentication".to_string(),
    };
    io::Error::new(io::ErrorKind::UnexpectedEof, message)
}

fn is_pre_authentication_disconnect(error: &io::Error) -> bool {
    matches!(
        error.raw_os_error().map(i32::cast_unsigned),
        Some(ERROR_BROKEN_PIPE | ERROR_NO_DATA | ERROR_PIPE_NOT_CONNECTED)
    )
}

pub(super) fn split(
    stream: LocalStream,
) -> (
    tokio::io::ReadHalf<LocalStream>,
    tokio::io::WriteHalf<LocalStream>,
) {
    tokio::io::split(stream)
}

View on GitHub (pinned to affd8760f4)

Solutions

  1. Treat this as a benign remote-side disconnect: server-side, catch io::ErrorKind::UnexpectedEof and continue the accept loop instead of failing
  2. Investigate the client: ensure it runs the full authentication handshake before dropping the connection
  3. If it happens at scale, check for scanners/load balancers probing the pipe and exclude the endpoint from probing

Example fix

// server accept loop
match listener.accept().await {
    Ok((stream, _)) => spawn_auth(stream),
    Err(e) if is_pre_authentication_disconnect(&e) => continue, // client hung up early
    Err(e) => return Err(e),
}
Defensive patterns

Strategy: try-catch

Try / catch

match listener.accept().await {
    Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => continue, // client hung up pre-auth
    other => other?,
}

Prevention

When it happens

Trigger: accept() reads from a freshly connected pipe client and the client process exits or its connection breaks (ERROR_BROKEN_PIPE, ERROR_NO_DATA, ERROR_PIPE_NOT_CONNECTED) before the handshake/auth exchange finishes.

Common situations: A health-check or port scanner connects and immediately closes; the client crashed or was killed mid-handshake; a client with the wrong protocol gives up before authenticating; network/pipe interruption during startup.

Understand the failure class

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/5eed94fe0c3d003f. Report an issue: GitHub.