actix/actix-web · critical
failed to build Hickory DNS resolver
Error message
failed to build Hickory DNS resolver
What it means
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`).
Source
Thrown at awc/src/client/connector.rs:1108
}
// get from thread local or construct a new hickory dns resolver.
HICKORY_DNS_RESOLVER.with(|local| {
local
.get_or_init(|| {
let (cfg, opts) = match read_system_conf() {
Ok((cfg, opts)) => (cfg, opts),
Err(err) => {
log::error!("Hickory DNS can not load system config: {err}");
(ResolverConfig::default(), ResolverOpts::default())
}
};
let resolver =
TokioResolver::builder_with_config(cfg, TokioRuntimeProvider::default())
.with_options(opts)
.build()
.expect("failed to build Hickory DNS resolver");
Resolver::custom(HickoryDnsResolver(resolver))
})
.clone()
})
}
}
#[cfg(feature = "dangerous-h2c")]
#[cfg(test)]
mod tests {
use std::convert::Infallible;
use actix_http::{HttpService, Request, Response, Version};
use actix_http_test::test_server;
use actix_service::ServiceFactoryExt as _;
use super::*;View on GitHub (pinned to 937960ca67)
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.
Example fix
// before - client built outside any runtime, panics on first use
static CLIENT: Lazy<Client> = Lazy::new(Client::default);
fn main() {
CLIENT.get("https://example.com").send(); // panic: failed to build Hickory DNS resolver
}
// after - build within the actix runtime
#[actix_rt::main]
async fn main() {
let client = Client::default(); // runtime is live, resolver spawns fine
let _ = client.get("https://example.com").send().await;
}
// tests must also use the actix runtime
#[actix_rt::test]
async fn fetches_ok() { /* ... */ } Defensive patterns
Strategy: validation
Validate before calling
// Run before constructing an awc::Connector when hickory-dns is enabled.
// The resolver needs a live Tokio runtime on the current thread.
fn tokio_runtime_present() -> bool {
tokio::runtime::Handle::try_current().is_ok()
}
if !tokio_runtime_present() {
panic!("awc Connector with hickory-dns must be built inside an actix-rt/tokio runtime.");
} Try / catch
// Panic via .expect(); cannot be a normal Result catch. catch_unwind works as a guard
// but the durable fix is to build the client inside the runtime:
use std::panic;
let client = panic::catch_unwind(|| {
// must run on a runtime thread
awc::Client::default()
});
match client {
Ok(c) => c,
Err(_) => { /* move construction into #[actix_rt::main] / an async block */ }
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Unsupported HTTP version: {:?}.
- actix-http client only supports versions http/1.1 & http/2
- All default headers must be added before cloning.
- Invalid header name
- Invalid header value
AI-assisted analysis of actix/actix-web@937960ca67 (2026-08-06).
Data as JSON: /data/errors/968e76e5cb8e86bc.json.
Report an issue: GitHub.