{"record":{"id":"cdcdbcf305a6e54a","repo":"denoland/deno","slug":"missing-host-in-uri","errorCode":null,"errorMessage":"missing host in URI","messagePattern":"missing host in URI","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"ext/fetch/dns.rs","lineNumber":308,"sourceCode":"\n  fn poll_ready(\n    &mut self,\n    _cx: &mut task::Context<'_>,\n  ) -> Poll<Result<(), Self::Error>> {\n    Poll::Ready(Ok(()))\n  }\n\n  fn call(&mut self, uri: Uri) -> Self::Future {\n    let this = self.clone();\n    Box::pin(async move {\n      let Some(permissions) = &this.permissions else {\n        let mut connector = this.http_connector(this.resolver.clone());\n        return connector.call(uri).await.map_err(Into::into);\n      };\n\n      let Some((bare_host, port)) = bare_host_and_port(&uri) else {\n        return Err(\n          io::Error::new(io::ErrorKind::InvalidInput, \"missing host in URI\")\n            .into(),\n        );\n      };\n      if let Ok(ip) = bare_host.parse::<IpAddr>() {\n        // IP literal: `HttpConnector` connects to it directly without\n        // consulting the resolver.\n        check_resolved(permissions, this.deny_check_kind, &ip, port)?;\n        let mut connector = this.http_connector(this.resolver.clone());\n        return connector.call(uri).await.map_err(Into::into);\n      }\n\n      let name = Name::from_str(bare_host).map_err(|e| -> BoxError {\n        io::Error::new(io::ErrorKind::InvalidInput, e.to_string()).into()\n      })?;\n      let addrs: Vec<SocketAddr> = this\n        .resolver\n        .clone()\n        .call(name)","sourceCodeStart":290,"sourceCodeEnd":326,"githubUrl":"https://github.com/denoland/deno/blob/9ad36f7a2cce60488e6ec52283efb32efddaf93a/ext/fetch/dns.rs#L290-L326","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nconst res = await fetch(req.url); // req.url === '/api/users'\n// Error: missing host in URI\n\n// after\nconst base = 'http://api.internal:8080';\nconst res = await fetch(new URL(req.url, base));","handlingStrategy":"validation","validationCode":"function assertFetchableUrl(input: string | URL, base?: string | URL): string {\n  const u = new URL(input, base); // throws TypeError on relative/invalid input with no base\n  if (!u.host) throw new TypeError(`URL has no host: ${String(input)}`);\n  return u.toString();\n}\n\nconst res = await fetch(assertFetchableUrl(req.url, 'http://api.internal:8080'));","typeGuard":"function hasHost(input: string | URL): boolean {\n  try {\n    return new URL(input).host !== '';\n  } catch {\n    return false;\n  }\n}","tryCatchPattern":"try {\n  await fetch(url);\n} catch (e) {\n  if (e instanceof TypeError && /missing host in URI/i.test(e.message)) {\n    throw new Error(`Relative or host-less URL passed to fetch: ${String(url)}`);\n  }\n  throw e;\n}","preventionTips":["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"],"tags":["fetch","url","hyper","permissions","invalid-input"],"backgroundTag":"invalid-request-url","analyzedSha":"9ad36f7a2cce60488e6ec52283efb32efddaf93a","analyzedAt":"2026-08-20T13:07:44.778Z","contentChangedAt":"2026-08-20T13:07:44.778Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}