actix/actix-web · critical
failed to build Hickory DNS resolver
Error message
failed to build Hickory DNS resolver
What it means
This is a panic (via .expect) inside awc's resolver module when the `hickory-dns` Cargo feature is enabled. The resolver is constructed lazily in a thread-local OnceCell (connector.rs:1088-1093); on first use per thread it calls `TokioResolver::builder_with_config(cfg, opts).build().expect("failed to build Hickory DNS resolver")` (connector.rs:1104-1108). Note the code already handles a failed read_system_conf() by falling back to `ResolverConfig::default()` (connector.rs:1096-1102), so the panic is specifically from TokioResolver.build() failing — i.e. an error building the async resolver from a (possibly defaulted) config, not from parsing resolv.conf. The panic therefore indicates a lower-level runtime/provider/IO problem at resolver construction.
Solutions
- Disable the `hickory-dns` (and legacy `trust-dns`) feature and use awc's default resolver (getaddrinfo-based Resolver::default()), which has no build() step and no extra runtime dependency. This removes the panic entirely.
- Ensure the process runs inside a tokio multi-thread runtime (the hickory TokioRuntimeProvider requires it); if you use a single-threaded/current-thread runtime, switch to the default resolver or a proper tokio runtime.
- Pin compatible versions: keep hickory-resolver at the version awc expects (0.26.1) and ensure tokio is the version hickory 0.26 was built against; resolve Cargo.lock conflicts via `cargo update -p hickory-resolver`.
- If you must keep hickory-dns, provide a valid /etc/resolv.conf and verify the resolver can be built outside awc first (see validationCode) to isolate whether the failure is environmental.
- As a last resort, supply a custom Resolver (Resolver::custom(...)) to awc::Connector so the hickory build path is never taken.
Example fix
# before: awc depends on hickory for DNS
awc = { version = "4", features = ["hickory-dns"] }
# after: use the default getaddrinfo resolver (no hickory, no panic path)
awc = { version = "4" }
# or, in code, avoid the default hickory resolver by passing a custom one:
# let connector = awc::Connector::new().resolver(my_resolver); Defensive patterns
Strategy: fallback
Validate before calling
// Validate that a hickory TokioResolver can be built BEFORE awc tries to,
// by attempting the same construction read_system_conf + build() sequence.
// Run this at startup when the `hickory-dns` feature is enabled.
use hickory_resolver::{
config::{ResolverConfig, ResolverOpts},
system_conf::read_system_conf,
TokioResolver, async_resolver::TokioRuntimeProvider,
};
fn hickory_builds_ok() -> bool {
let (cfg, opts) = read_system_conf()
.unwrap_or((ResolverConfig::default(), ResolverOpts::default()));
TokioResolver::builder_with_config(cfg, TokioRuntimeProvider::default())
.with_options(opts)
.build()
.is_ok()
}
if !hickory_builds_ok() {
panic!("hickory DNS resolver cannot be built; disable the hickory-dns feature or fix the runtime");
} Type guard
// No type guard: feature-gated and panics at runtime inside a thread-local. // Detect via the standalone build() probe above instead. // null
Try / catch
// awc's resolver() panics inside a thread-local OnceCell; you cannot catch it
// directly because it runs lazily on first DNS use. Prevent it by constructing
// the resolver yourself and passing a custom Resolver, so the panic path is
// never taken:
use actix_tls::connect::Resolver;
use std::panic;
let client = match panic::catch_unwind(|| {
// verify hickory builds; if it panics, fall back to default GAI resolver
awc::Client::new()
}) {
Ok(c) => c,
Err(_) => {
// explicit default resolver avoids the hickory code path entirely
awc::Client::builder()
.connector(awc::Connector::new()) // Resolver::default(), no hickory
.finish()
}
};
// Best practice: do NOT enable the hickory-dns feature unless you need it. Prevention
- Do not enable `hickory-dns`/`trust-dns` unless you specifically need it; the default getaddrinfo resolver has no build() panic path.
- Ensure the process runs on a tokio multi-thread runtime; hickory's TokioRuntimeProvider requires it.
- Keep hickory-resolver at the version awc expects (0.26.1) and resolve Cargo.lock version conflicts.
- At startup, run the standalone build() probe (see validationCode) to fail fast with a clear message.
- If you ship to locked-down/container environments, prefer the default resolver or pass an explicit Resolver::custom to awc::Connector to bypass the hickory thread-local entirely.
When it happens
Trigger: Enabling awc's `hickory-dns` (or the deprecated `trust-dns`) feature and performing the first DNS resolution on a given worker thread, which triggers the thread-local OnceCell initialization and the TokioResolver::build() call. The panic surfaces as soon as a client tries to resolve a hostname and the cached resolver does not yet exist on that thread. read_system_conf failure is tolerated, so the trigger is build() itself returning Err.
Common situations: Containers where the tokio runtime provider for hickory cannot function correctly (missing network namespaces, restricted syscalls); version incompatibilities between hickory-resolver and the tokio runtime (e.g. awc's hickory-resolver 0.26.1 pinned against an incompatible tokio); misconfigured or absent /etc/resolv.conf combined with a build() that still errors; async runtimes not backed by tokio (hickory's TokioRuntimeProvider requires a tokio context); extremely locked-down environments where resolver background tasks cannot spawn.
Related errors
- Failed to find native root certificates
- actix-http client only supports versions http/1.1 & http/2
- Response Payload IO timed out
AI-assisted analysis of actix/actix-web@4d435abc28 (2026-08-09).
Data as JSON: /api/errors/968e76e5cb8e86bc.
Report an issue: GitHub.
Appendix: 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 4d435abc28)