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

  1. Resolve relative URLs against a known base before fetching: new URL(path, base)
  2. Validate up front that the URL has a host: const u = new URL(input); if (!u.host) throw ...
  3. In middleware, reconstruct the outgoing Request with the absolute URL
  4. 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

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


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/cdcdbcf305a6e54a. Report an issue: GitHub.