ruvnet/ruflo · warning · Error

Invalid hostname

Error message

Invalid hostname

What it means

Thrown by assertValidHostname in isURLLocal.ts when the URL hostname is either empty or exceeds the RFC 1035 maximum of 253 characters. isURLLocal calls this guard only when URL.hostname is not a literal IP (checked via node:net isIP), so the value is treated as a DNS name and must satisfy DNS length limits before dns.lookup is invoked. This is the first line of defense before any SSRF check.

Source

Thrown at ruflo/src/ruvocal/src/lib/server/isURLLocal.ts:16

import { Address6, Address4 } from "ip-address";
import dns from "node:dns";
import { isIP } from "node:net";

const dnsLookup = (hostname: string): Promise<{ address: string; family: number }> => {
	return new Promise((resolve, reject) => {
		dns.lookup(hostname, (err, address, family) => {
			if (err) return reject(err);
			resolve({ address, family });
		});
	});
};

function assertValidHostname(hostname: string): void {
	if (!hostname || hostname.length > 253) {
		throw new Error("Invalid hostname");
	}

	const labels = hostname.split(".");

	for (const label of labels) {
		if (!label || label.length > 63) {
			throw new Error("Invalid hostname");
		}

		if (!/^[A-Za-z0-9-]+$/.test(label)) {
			throw new Error("Invalid hostname");
		}

		if (label.startsWith("-") || label.endsWith("-")) {
			throw new Error("Invalid hostname");
		}
	}
}

View on GitHub (pinned to 6b01dc5a68)

Solutions

  1. Validate or sanitize the hostname (length 1-253, RFC 1035 labels) before calling isURLLocal.
  2. If the host can legitimately be empty, short-circuit before calling isURLLocal and treat it as invalid input rather than relying on the throw.
  3. For IDN hostnames, normalize to punycode (url.hostname already returns ASCII via WHATWG URL) and confirm the encoded length is within 253.

Example fix

// before
const local = await isURLLocal(new URL(userInput));

// after
const u = new URL(userInput);
if (!u.hostname || u.hostname.length > 253) {
  return { valid: false, reason: 'invalid-host' };
}
const local = await isURLLocal(u);
Defensive patterns

Strategy: validation

Validate before calling

import { isIP } from "node:net";

function isValidDnsHostname(host: string): boolean {
  if (!host || host.length > 253) return false;
  if (isIP(host)) return true; // IP literals skip assertValidHostname
  const labels = host.split(".");
  for (const l of labels) {
    if (!l || l.length > 63) return false;
  }
  return true;
}

if (!isValidDnsHostname(url.hostname)) return { valid: false };

Type guard

function isPlausibleHostname(host: string): host is string {
  return typeof host === "string" && host.length > 0 && host.length <= 253 && !host.includes("..");
}

Try / catch

try {
  const local = await isURLLocal(url);
} catch (e) {
  if (e instanceof Error && e.message === "Invalid hostname") {
    // reject the input; do not fall through to default-local
    return { valid: false, reason: "invalid-hostname" };
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling isURLLocal(url) or isURLStringLocal(urlStr) with a URL whose hostname is '' (e.g. 'http:///path' or 'file:///path') or longer than 253 bytes (e.g. a very long punycoded IDN, or a data: URL coerced into a URL object). Also reachable when a fetch-url endpoint or conversation request supplies a malformed host.

Common situations: Bugs that build URLs from untrusted user input without validating the host; IDN/Unicode hostnames that expand past 253 bytes once punycoded; copy-pasted URLs with extra path data mistakenly placed in the host slot; tests that pass synthetic URLs like new URL('http://').

Related errors


AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12). Data as JSON: /api/errors/f9129b3a3a205266. Report an issue: GitHub.