sigoden/aichat · error · Panic

Failed to install CTRL+C signal handler

Error message

Failed to install CTRL+C signal handler

What it means

The server's graceful-shutdown helper calls tokio::signal::ctrl_c() and unwraps with this message; the OS refused to install the SIGINT handler. tokio returns an error here only when the signal cannot be registered with the runtime/OS — essentially never in normal operation.

Solutions

  1. Enable the tokio "signal" feature in Cargo.toml (tokio = { version = "1", features = ["signal", "full"] }).
  2. Run the server inside a normal #[tokio::main] runtime so the signal driver is available.
  3. If the platform cannot register SIGINT, replace the .expect with graceful degradation: log and fall back to terminating without a handler.

Example fix

// before
async fn shutdown_signal() {
    tokio::signal::ctrl_c().await.expect("Failed to install CTRL+C signal handler")
}
// after
async fn shutdown_signal() {
    if tokio::signal::ctrl_c().await.is_err() {
        eprintln!("SIGINT handler unavailable; press Ctrl+C to kill");
        std::future::pending::<()>().await;
    }
}
Defensive patterns

Strategy: try-catch

Try / catch

match tokio::signal::ctrl_c().await {
    Ok(()) => tracing::info!("shutting down on SIGINT"),
    Err(e) => tracing::warn!("ctrl_c handler unavailable: {e}"),
}

Prevention

When it happens

Trigger: Running outside a tokio runtime context where the signal driver is unavailable; operating systems or sandboxes that block SIGINT registration (some restricted containers, exotic platforms); tokio runtime built without the "signal" feature.

Common situations: Embedding the serve binary inside another runtime or test harness without enabling tokio's signal feature; minimal container images with restricted seccomp profiles; custom tokio runtime configurations.


AI-assisted analysis of sigoden/aichat@82976d349a (2026-09-09). Data as JSON: /api/errors/aa9bba9941d9a2ff. Report an issue: GitHub.

Appendix: source

Thrown at src/serve.rs:629

struct RerankReqBody {
    documents: Vec<String>,
    query: String,
    model: String,
    top_n: Option<usize>,
}

#[derive(Debug)]
enum ResEvent {
    First(Option<String>),
    Text(String),
    ToolCalls(Vec<ToolCall>),
    Done,
}

async fn shutdown_signal() {
    tokio::signal::ctrl_c()
        .await
        .expect("Failed to install CTRL+C signal handler")
}

fn generate_completion_id() -> String {
    let random_id = chrono::Utc::now().nanosecond();
    format!("chatcmpl-{random_id}")
}

fn set_cors_header(res: &mut AppResponse) {
    res.headers_mut().insert(
        hyper::header::ACCESS_CONTROL_ALLOW_ORIGIN,
        hyper::header::HeaderValue::from_static("*"),
    );
    res.headers_mut().insert(
        hyper::header::ACCESS_CONTROL_ALLOW_METHODS,
        hyper::header::HeaderValue::from_static("GET,POST,PUT,PATCH,DELETE"),
    );
    res.headers_mut().insert(
        hyper::header::ACCESS_CONTROL_ALLOW_HEADERS,

View on GitHub (pinned to 82976d349a)