clockworklabs/SpacetimeDB · error · io::Error
refusing to connect to private or special-purpose addresses
Error message
refusing to connect to private or special-purpose addresses
What it means
SpacetimeDB's host applies SSRF protection to every outgoing HTTP(S) request a database module makes. URLs with an IP-literal host are checked directly, and hostnames resolve through FilteredDnsResolver, which drops every blocked address - RFC 6890 special-purpose ranges including 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16, documentation/benchmarking ranges, and their IPv6 analogues. If no allowed address remains, the request fails with PermissionDenied and this message.
Source
Thrown at crates/core/src/host/instance_env.rs:1036
///
/// If the user requests a timeout longer than this, we will clamp to this value.
/// 180 seconds accommodates long-running LLM and AI API calls,
/// which routinely take 30-120 seconds for complex requests.
const HTTP_MAX_TIMEOUT: Duration = Duration::from_secs(180);
const BLOCKED_HTTP_ADDRESS_ERROR: &str = "refusing to connect to private or special-purpose addresses";
struct FilteredDnsResolver;
impl reqwest::dns::Resolve for FilteredDnsResolver {
fn resolve(&self, name: reqwest::dns::Name) -> reqwest::dns::Resolving {
let host = name.as_str().to_owned();
Box::pin(async move {
let addrs = tokio::net::lookup_host((host.as_str(), 0)).await?;
let filtered_addrs: Vec<SocketAddr> = addrs.filter(|addr| !is_blocked_ip(addr.ip())).collect();
if filtered_addrs.is_empty() {
return Err(
std::io::Error::new(std::io::ErrorKind::PermissionDenied, BLOCKED_HTTP_ADDRESS_ERROR).into(),
);
}
Ok(Box::new(filtered_addrs.into_iter()) as reqwest::dns::Addrs)
})
}
}
fn is_blocked_ip_literal(url: &reqwest::Url) -> bool {
match url.host() {
Some(url::Host::Ipv4(ip)) => is_blocked_ip(IpAddr::V4(ip)),
Some(url::Host::Ipv6(ip)) => is_blocked_ip(IpAddr::V6(ip)),
Some(url::Host::Domain(_)) | None => false,
}
}
fn is_blocked_ip(ip: IpAddr) -> bool {
match ip {View on GitHub (pinned to 524b4487d9)
Solutions
- Point the module at a public, externally reachable address instead.
- If internal egress is genuinely required, route through a public proxy/gateway you control that performs its own authentication.
- For local test builds only, the host can be compiled with the allow_loopback_http_for_tests feature to unblock loopback - never enable this in production.
Example fix
// before: dev URL shipped into a hosted module let url = "http://localhost:8080/api"; // after: public endpoint reachable from the host's egress let url = "https://api.example.com/v1";
Defensive patterns
Strategy: validation
Validate before calling
use std::net::IpAddr;
fn is_private_or_special(ip: IpAddr) -> bool {
match ip {
IpAddr::V4(v) => v.is_loopback() || v.is_private() || v.is_link_local() || v.is_unspecified(),
IpAddr::V6(v) => v.is_loopback() || v.is_unspecified() || (v.segments()[0] & 0xfe00) == 0xfc00,
}
}
// Resolve the target host first and fail fast with your own message if every
// address is blocked, instead of discovering it inside the module call. Try / catch
match module_http_request(url).await {
Err(e) if e.to_string().contains("private or special-purpose addresses") => {
// Configuration error, not transient: surface it to the operator and point
// the module at a public endpoint or an authenticated proxy.
}
r => r,
} Prevention
- Never point module HTTP calls at localhost, 127.0.0.1, link-local, or RFC1918 addresses.
- Remember DNS names resolving only to private IPs are also blocked.
- Keep dev-time localhost URLs out of deployed module configuration.
When it happens
Trigger: A module HTTP client requesting http://localhost:PORT, http://127.0.0.1, http://169.254.169.254/ (cloud metadata), any 10.x / 172.16-31.x / 192.168.x address, or a DNS name that resolves only to private or loopback IPs (e.g. an internal cluster service name).
Common situations: A module calling its own SpacetimeDB node or a sibling internal service during development; webhooks pointing at internal hostnames; localhost dev URLs accidentally shipped to a hosted deployment.
Related errors
- Aborted
- Error fetching public key for issuer {raw_issuer}
- Unrecognized extra bytes while decoding BSATN value
- Invalid HTTP method returned from host
- Invalid HTTP version returned from host
AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16).
Data as JSON: /api/errors/86a50f55bd92d8d6.
Report an issue: GitHub.