denoland/deno · error
missing host in URI
Error message
missing host in URI
What it means
PermissionedHttpConnector (ext/fetch/dns.rs) is the hyper connector Deno uses for fetch() and module loading when permission checks are active. Before resolving DNS it extracts host and port from the request URI via bare_host_and_port(); if uri.host() is None it cannot run the --allow-net/--deny-net check and fails with io::ErrorKind::InvalidInput 'missing host in URI'. Without a PermissionsContainer this arm is skipped entirely.
Source
Thrown at ext/fetch/dns.rs:308
fn poll_ready(
&mut self,
_cx: &mut task::Context<'_>,
) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, uri: Uri) -> Self::Future {
let this = self.clone();
Box::pin(async move {
let Some(permissions) = &this.permissions else {
let mut connector = this.http_connector(this.resolver.clone());
return connector.call(uri).await.map_err(Into::into);
};
let Some((bare_host, port)) = bare_host_and_port(&uri) else {
return Err(
io::Error::new(io::ErrorKind::InvalidInput, "missing host in URI")
.into(),
);
};
if let Ok(ip) = bare_host.parse::<IpAddr>() {
// IP literal: `HttpConnector` connects to it directly without
// consulting the resolver.
check_resolved(permissions, this.deny_check_kind, &ip, port)?;
let mut connector = this.http_connector(this.resolver.clone());
return connector.call(uri).await.map_err(Into::into);
}
let name = Name::from_str(bare_host).map_err(|e| -> BoxError {
io::Error::new(io::ErrorKind::InvalidInput, e.to_string()).into()
})?;
let addrs: Vec<SocketAddr> = this
.resolver
.clone()
.call(name)View on GitHub (pinned to 9ad36f7a2c)
Solutions
- Resolve relative URLs against a known base before fetching: new URL(path, base)
- Validate up front that the URL has a host: const u = new URL(input); if (!u.host) throw ...
- In middleware, reconstruct the outgoing Request with the absolute URL
- Check for code that rewrites request URLs and drops the authority
Example fix
// before const res = await fetch(req.url); // req.url === '/api/users' // Error: missing host in URI // after const base = 'http://api.internal:8080'; const res = await fetch(new URL(req.url, base));
Defensive patterns
Strategy: validation
Validate before calling
function assertFetchableUrl(input: string | URL, base?: string | URL): string {
const u = new URL(input, base); // throws TypeError on relative/invalid input with no base
if (!u.host) throw new TypeError(`URL has no host: ${String(input)}`);
return u.toString();
}
const res = await fetch(assertFetchableUrl(req.url, 'http://api.internal:8080')); Type guard
function hasHost(input: string | URL): boolean {
try {
return new URL(input).host !== '';
} catch {
return false;
}
} Try / catch
try {
await fetch(url);
} catch (e) {
if (e instanceof TypeError && /missing host in URI/i.test(e.message)) {
throw new Error(`Relative or host-less URL passed to fetch: ${String(url)}`);
}
throw e;
} Prevention
- Always construct request URLs with new URL(path, base) so the authority is guaranteed
- Never forward req.url directly in proxy middleware without resolving it first
- Assert URL.host in one place at the edge of your HTTP layer
When it happens
Trigger: fetch() (or module import) through the permission-checking connector with a URI that has no authority component: relative-form URIs ('/api/x'), scheme-only URIs ('http://'), authority-form CONNECT targets, or a Request whose url was rewritten to a path by middleware/proxy code.
Common situations: A fetch wrapper doing fetch(req.url) where req.url is relative; string-built URLs like 'http://' + host + path when host is empty; test doubles that skip URL resolution; proxy handlers forwarding the raw path instead of the absolute URL.
Related errors
- Can't convert url ("{}") to filename.
- Can't convert url ("{}") to filename.
- cannot read url: request closed
- Cannot read url: request closed
- Referrer "${referrer}" is not a valid URL.
AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20).
Data as JSON: /api/errors/cdcdbcf305a6e54a.
Report an issue: GitHub.