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

  1. Construct the `Connector`/`Client` from inside the running actix/tokio runtime (e.g. inside an async block under `#[actix_rt::main]`).
  2. Annotate test entrypoints with `#[actix_rt::test]` (or `#[tokio::test]`) so the resolver builds with a live runtime on the thread.
  3. Defer client construction until first use within an async context instead of evaluating it eagerly in a `static` initializer.
  4. 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

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


AI-assisted analysis of actix/actix-web@937960ca67 (2026-08-06). Data as JSON: /data/errors/968e76e5cb8e86bc.json. Report an issue: GitHub.