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
- Resolve scraped hrefs against the page base URL before calling fetchExternalBrandAsset: new URL(href, pageUrl).toString().
- Filter candidates through a URL parse pre-check and skip unparseable ones rather than aborting the whole extraction.
- If the URL is user-supplied, require an absolute http(s) string at the API boundary and 400 the request otherwise.
- 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
- Resolve scraped hrefs against the page base URL before forwarding.
- Filter out non-http(s) and unparseable hrefs at the scraper.
- Wrap each asset fetch in try/catch so one bad URL does not abort extraction.
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
- unsupported brand asset protocol: ${parsed.protocol}
- blocked non-public brand asset host: ${host}
- too many brand asset redirects (> ${MAX_BRAND_REDIRECTS})
- brand asset host resolves to a non-public address: ${host} -
- Could not fetch ${url} — the site may block server-side requ
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/36d434f46afb92e2.
Report an issue: GitHub.