nikivdev/code · error

http client cache mutex poisoned

Error message

http client cache mutex poisoned

What it means

blocking_with_timeout caches reqwest blocking Clients keyed by timeout in a global Mutex; if another thread panicked while holding the lock, the Mutex is poisoned and lock() returns Err, producing this error. It surfaces a poisoned shared-state condition instead of propagating a poison panic.

Source

Thrown at src/http_client.rs:20

use std::sync::{Mutex, OnceLock};
use std::time::Duration;

use anyhow::{Context, Result};
use reqwest::blocking::Client;

static BLOCKING_CLIENTS: OnceLock<Mutex<HashMap<u64, Client>>> = OnceLock::new();

fn timeout_key(timeout: Duration) -> u64 {
    timeout.as_millis().min(u64::MAX as u128) as u64
}

/// Reuse blocking reqwest clients by timeout bucket to avoid repeated TLS/client init.
pub fn blocking_with_timeout(timeout: Duration) -> Result<Client> {
    let clients = BLOCKING_CLIENTS.get_or_init(|| Mutex::new(HashMap::new()));
    let key = timeout_key(timeout);
    let mut guard = clients
        .lock()
        .map_err(|_| anyhow::anyhow!("http client cache mutex poisoned"))?;

    if let Some(client) = guard.get(&key) {
        return Ok(client.clone());
    }

    let client = Client::builder()
        .timeout(timeout)
        .build()
        .with_context(|| format!("failed to build http client with timeout {:?}", timeout))?;
    guard.insert(key, client.clone());
    Ok(client)
}

View on GitHub (pinned to a747e741ae)

Solutions

  1. Fix the underlying panic that poisoned the mutex (check logs from the first panicking thread)
  2. Restart the process to clear the poisoned mutex
  3. Replace Mutex with a poisoning-tolerant pattern (lock, recover data with into_inner, or use a lock-free cache)

Example fix

// before
let mut guard = clients.lock().map_err(|_| anyhow::anyhow!("http client cache mutex poisoned"))?;
// after
let mut guard = match clients.lock() {
    Ok(g) => g,
    Err(poisoned) => poisoned.into_inner(), // recover cache instead of failing
};
Defensive patterns

Strategy: try-catch

Try / catch

match blocking_with_timeout(Duration::from_secs(30)) {
    Err(e) if e.to_string().contains("mutex poisoned") => {
        // restart process or bypass cache with a fresh Client::new()
        let client = reqwest::blocking::Client::builder().timeout(Duration::from_secs(30)).build()?;
    }
    other => other?,
}

Prevention

When it happens

Trigger: Any call to blocking_with_timeout after a thread panicked while holding the BLOCKING_CLIENTS mutex (e.g. panic inside client construction under the lock).

Common situations: A prior request thread panicked during TLS/client init, leaving the cache mutex poisoned for all subsequent requests in the process.

Related errors


AI-assisted analysis of nikivdev/code@a747e741ae (2026-09-01). Data as JSON: /api/errors/030a0d6613a7cf5d. Report an issue: GitHub.