{"id":"968e76e5cb8e86bc","repo":"actix/actix-web","slug":"failed-to-build-hickory-dns-resolver","errorCode":null,"errorMessage":"failed to build Hickory DNS resolver","messagePattern":"failed to build Hickory DNS resolver","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"awc/src/client/connector.rs","lineNumber":1108,"sourceCode":"        }\n\n        // get from thread local or construct a new hickory dns resolver.\n        HICKORY_DNS_RESOLVER.with(|local| {\n            local\n                .get_or_init(|| {\n                    let (cfg, opts) = match read_system_conf() {\n                        Ok((cfg, opts)) => (cfg, opts),\n                        Err(err) => {\n                            log::error!(\"Hickory DNS can not load system config: {err}\");\n                            (ResolverConfig::default(), ResolverOpts::default())\n                        }\n                    };\n\n                    let resolver =\n                        TokioResolver::builder_with_config(cfg, TokioRuntimeProvider::default())\n                            .with_options(opts)\n                            .build()\n                            .expect(\"failed to build Hickory DNS resolver\");\n\n                    Resolver::custom(HickoryDnsResolver(resolver))\n                })\n                .clone()\n        })\n    }\n}\n\n#[cfg(feature = \"dangerous-h2c\")]\n#[cfg(test)]\nmod tests {\n    use std::convert::Infallible;\n\n    use actix_http::{HttpService, Request, Response, Version};\n    use actix_http_test::test_server;\n    use actix_service::ServiceFactoryExt as _;\n\n    use super::*;","sourceCodeStart":1090,"sourceCodeEnd":1126,"githubUrl":"https://github.com/actix/actix-web/blob/937960ca67f20e14ffe2a075bf6d4593502be12c/awc/src/client/connector.rs#L1090-L1126","documentation":"Raised by `.expect(\"failed to build Hickory DNS resolver\")` at awc/src/client/connector.rs:1108, inside the thread-local initializer used only when the `hickory-dns` (or deprecated `trust-dns`) Cargo feature is enabled. The code reads system DNS config and already falls back to `ResolverConfig::default()` if `read_system_conf()` fails, so the config is never the problem - the panic comes from `TokioResolver::...build()` itself returning `Err`, almost always because there is no Tokio runtime on the current thread to spawn the resolver's background tasks (it is built with `TokioRuntimeProvider`).","triggerScenarios":"First use of the thread-local Hickory resolver on a thread that is not running inside an actix-rt/tokio runtime - e.g. constructing an `awc::Connector`/`Client` eagerly in a `static`/`LazyLock`, in a plain `fn main()` before the runtime starts, in a `#[test]` that is not `#[actix_rt::test]`, or inside a manually spawned thread lacking a runtime guard.","commonSituations":"Initializing the HTTP client in `LazyLock`/`once_cell` at module scope; unit tests using the default `#[test]` macro instead of the actix/tokio test attribute; calling `Client::default()` from a blocking thread or a `std::thread::spawn` worker; framework glue that builds the client during synchronous startup before `#[actix_rt::main]` has entered the runtime.","solutions":["Construct the `Connector`/`Client` from inside the running actix/tokio runtime (e.g. inside an async block under `#[actix_rt::main]`).","Annotate test entrypoints with `#[actix_rt::test]` (or `#[tokio::test]`) so the resolver builds with a live runtime on the thread.","Defer client construction until first use within an async context instead of evaluating it eagerly in a `static` initializer.","If a runtime is genuinely unavailable on that thread, drop the `hickory-dns`/`trust-dns` feature and use the default `Resolver`, which does not spawn background resolver tasks."],"exampleFix":"// before - client built outside any runtime, panics on first use\nstatic CLIENT: Lazy<Client> = Lazy::new(Client::default);\n\nfn main() {\n    CLIENT.get(\"https://example.com\").send(); // panic: failed to build Hickory DNS resolver\n}\n\n// after - build within the actix runtime\n#[actix_rt::main]\nasync fn main() {\n    let client = Client::default(); // runtime is live, resolver spawns fine\n    let _ = client.get(\"https://example.com\").send().await;\n}\n\n// tests must also use the actix runtime\n#[actix_rt::test]\nasync fn fetches_ok() { /* ... */ }","handlingStrategy":"validation","validationCode":"// Run before constructing an awc::Connector when hickory-dns is enabled.\n// The resolver needs a live Tokio runtime on the current thread.\nfn tokio_runtime_present() -> bool {\n    tokio::runtime::Handle::try_current().is_ok()\n}\n\nif !tokio_runtime_present() {\n    panic!(\"awc Connector with hickory-dns must be built inside an actix-rt/tokio runtime.\");\n}","typeGuard":null,"tryCatchPattern":"// Panic via .expect(); cannot be a normal Result catch. catch_unwind works as a guard\n// but the durable fix is to build the client inside the runtime:\nuse std::panic;\nlet client = panic::catch_unwind(|| {\n    // must run on a runtime thread\n    awc::Client::default()\n});\nmatch client {\n    Ok(c) => c,\n    Err(_) => { /* move construction into #[actix_rt::main] / an async block */ }\n}","preventionTips":["Never put `Client::default()` in a `static`/`LazyLock` initializer; build it inside your async entrypoint.","Mark every test that touches awc with `#[actix_rt::test]`.","If you spawn helper threads, enter the runtime handle (`handle.enter()`) or disable the `hickory-dns` feature for those paths.","Treat the panic message as a configuration smell: it always means client construction happened off-runtime."],"tags":["dns","hickory","tokio","runtime","panic","startup"],"analyzedSha":"937960ca67f20e14ffe2a075bf6d4593502be12c","analyzedAt":"2026-08-06T01:15:46.978Z","schemaVersion":2}