{"record":{"id":"030a0d6613a7cf5d","repo":"nikivdev/code","slug":"http-client-cache-mutex-poisoned","errorCode":null,"errorMessage":"http client cache mutex poisoned","messagePattern":"http client cache mutex poisoned","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/http_client.rs","lineNumber":20,"sourceCode":"use std::sync::{Mutex, OnceLock};\nuse std::time::Duration;\n\nuse anyhow::{Context, Result};\nuse reqwest::blocking::Client;\n\nstatic BLOCKING_CLIENTS: OnceLock<Mutex<HashMap<u64, Client>>> = OnceLock::new();\n\nfn timeout_key(timeout: Duration) -> u64 {\n    timeout.as_millis().min(u64::MAX as u128) as u64\n}\n\n/// Reuse blocking reqwest clients by timeout bucket to avoid repeated TLS/client init.\npub fn blocking_with_timeout(timeout: Duration) -> Result<Client> {\n    let clients = BLOCKING_CLIENTS.get_or_init(|| Mutex::new(HashMap::new()));\n    let key = timeout_key(timeout);\n    let mut guard = clients\n        .lock()\n        .map_err(|_| anyhow::anyhow!(\"http client cache mutex poisoned\"))?;\n\n    if let Some(client) = guard.get(&key) {\n        return Ok(client.clone());\n    }\n\n    let client = Client::builder()\n        .timeout(timeout)\n        .build()\n        .with_context(|| format!(\"failed to build http client with timeout {:?}\", timeout))?;\n    guard.insert(key, client.clone());\n    Ok(client)\n}\n","sourceCodeStart":2,"sourceCodeEnd":33,"githubUrl":"https://github.com/nikivdev/code/blob/a747e741ae92c09071d0ae946ab48488adcff1ce/src/http_client.rs#L2-L33","documentation":"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.","triggerScenarios":"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).","commonSituations":"A prior request thread panicked during TLS/client init, leaving the cache mutex poisoned for all subsequent requests in the process.","solutions":["Fix the underlying panic that poisoned the mutex (check logs from the first panicking thread)","Restart the process to clear the poisoned mutex","Replace Mutex with a poisoning-tolerant pattern (lock, recover data with into_inner, or use a lock-free cache)"],"exampleFix":"// before\nlet mut guard = clients.lock().map_err(|_| anyhow::anyhow!(\"http client cache mutex poisoned\"))?;\n// after\nlet mut guard = match clients.lock() {\n    Ok(g) => g,\n    Err(poisoned) => poisoned.into_inner(), // recover cache instead of failing\n};","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"match blocking_with_timeout(Duration::from_secs(30)) {\n    Err(e) if e.to_string().contains(\"mutex poisoned\") => {\n        // restart process or bypass cache with a fresh Client::new()\n        let client = reqwest::blocking::Client::builder().timeout(Duration::from_secs(30)).build()?;\n    }\n    other => other?,\n}","preventionTips":["Never panic while holding the client-cache mutex","Use into_inner() recovery instead of failing on poison","Keep client construction fallible (use ?) rather than unwrap under the lock"],"tags":["concurrency","mutex","http"],"backgroundTag":"mutex-poisoned","analyzedSha":"a747e741ae92c09071d0ae946ab48488adcff1ce","analyzedAt":"2026-09-01T22:43:55.719Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}