nexu-io/open-design · error · Error

invalid brand asset url: ${String(url)}

Error message

invalid brand asset url: ${String(url)}

What it means

Thrown by assertPublicBrandUrl when `new URL(url)` raises — the brand asset URL cannot be parsed at all. assertPublicBrandUrl is the SSRF pre-check installed in front of every brand outbound fetch (logo/imagery/seed/font fallbacks, prefetch, library route), so a malformed URL aborts before any network call.

Source

Thrown at apps/daemon/src/brands/safe-fetch.ts:62

  //   IPv4 link-local/metadata (169.254), IPv4 multicast (>=224), `::`,
  //   IPv6 link-local (fe80::/10) and ULA (fc00::/7).
  return isLoopbackApiHost(h) || isBlockedExternalApiHostname(h);
}

function isIpLiteral(host: string): boolean {
  return /^\d{1,3}(?:\.\d{1,3}){3}$/.test(host) || host.includes(':');
}

/**
 * Throw unless `url` is an http(s) URL whose host is a public address — checked
 * both as the literal host and, for a hostname, against every DNS answer.
 */
export async function assertPublicBrandUrl(url: string): Promise<void> {
  let parsed: URL;
  try {
    parsed = new URL(url);
  } catch {
    throw new Error(`invalid brand asset url: ${String(url)}`);
  }
  if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
    throw new Error(`unsupported brand asset protocol: ${parsed.protocol}`);
  }
  const host = parsed.hostname.replace(/^\[/, '').replace(/\]$/, '').toLowerCase();
  if (isNonPublicHost(host)) {
    throw new Error(`blocked non-public brand asset host: ${host}`);
  }
  if (!isIpLiteral(host)) {
    let addresses: Array<{ address: string }>;
    try {
      addresses = await dnsPromises.lookup(host, { all: true });
    } catch {
      // Let the actual fetch surface a resolution failure rather than masking it.
      return;
    }
    for (const { address } of addresses) {
      if (isNonPublicHost(String(address))) {

View on GitHub (pinned to 5be4028344)

Solutions

  1. Resolve scraped hrefs against the page base URL before calling fetchExternalBrandAsset: new URL(href, pageUrl).toString().
  2. Filter candidates through a URL parse pre-check and skip unparseable ones rather than aborting the whole extraction.
  3. If the URL is user-supplied, require an absolute http(s) string at the API boundary and 400 the request otherwise.
  4. Wrap fetchExternalBrandAsset in try/catch per-asset so one bad URL does not kill the whole brand build.

Example fix

// before
const res = await fetchExternalBrandAsset(rawHref);
// after
let abs;
try { abs = new URL(rawHref, pageUrl).toString(); } catch { continue; }
const res = await fetchExternalBrandAsset(abs);
Defensive patterns

Strategy: validation

Validate before calling

function parseOrNull(url, base) {
  try { return new URL(url, base); } catch { return null; }
}
// skip unparseable hrefs instead of forwarding them
const abs = parseOrNull(rawHref, pageUrl);
if (!abs) continue;

Type guard

const isParsableUrl = (u: unknown, base?: string): u is string => {
  if (typeof u !== 'string' || !u) return false;
  try { new URL(u, base); return true; } catch { return false; }
};

Try / catch

try {
  await fetchExternalBrandAsset(absUrl);
} catch (e) {
  if (String(e.message).startsWith('invalid brand asset url')) {
    // skip this asset, keep extracting others
    continue;
  }
  throw e;
}

Prevention

When it happens

Trigger: fetchExternalBrandAsset is called with an empty string, a URL missing its scheme ('example.com/x.png'), a URL with whitespace/control chars, or garbage scraped from a page (e.g. an href of 'javascript:...' that bypassed earlier filtering, or undefined coerced to the string 'undefined').

Common situations: Page-scraping fallbacks encounter hrefs that look URL-ish but are not absolute; an LLM-produced brand.json lists a logo URL with a typo; a connector hands a relative href straight to the fetcher without resolving it against the page base.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/36d434f46afb92e2. Report an issue: GitHub.