nexu-io/open-design · warning · Error

too many brand asset redirects (> ${MAX_BRAND_REDIRECTS})

Error message

too many brand asset redirects (> ${MAX_BRAND_REDIRECTS})

What it means

fetchExternalBrandAsset follows redirects manually (redirect: 'manual') so every hop can be re-validated by assertPublicBrandUrl. After MAX_BRAND_REDIRECTS (5) hops, another 3xx with a Location throws rather than following further. This bounds redirect chains so an attacker cannot loop or exhaust the daemon.

Source

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

  let target = url;
  for (let hop = 0; ; hop += 1) {
    // Re-validate every hop's host (initial URL and each redirect target) before
    // the request, so a public site can't 3xx us into private space. The real
    // SSRF stop is the pinned dispatcher below; this is a cheap URL-level
    // pre-check (protocol, literal private IPs, redirect target).
    await assertPublicBrandUrl(target);
    const res = await fetch(target, {
      ...init,
      redirect: 'manual',
      dispatcher: brandAssetDispatcher as unknown as NonNullable<RequestInit['dispatcher']>,
    });
    const location =
      res.status >= 300 && res.status < 400 ? res.headers.get('location') : null;
    if (!location) return res;
    // Drain the redirect response body before following it or bailing out.
    if (res.body) await res.body.cancel().catch(() => {});
    if (hop >= MAX_BRAND_REDIRECTS) {
      throw new Error(`too many brand asset redirects (> ${MAX_BRAND_REDIRECTS})`);
    }
    // Resolve a possibly-relative Location; the next loop re-validates it and the
    // same pinned dispatcher re-binds the new connection.
    target = new URL(location, target).toString();
  }
}

View on GitHub (pinned to 5be4028344)

Solutions

  1. Resolve the final asset URL out-of-band (curl -I) and pass the terminal URL directly so no redirect chain is walked.
  2. If the chain is legitimate and short, confirm none of the intermediate hosts are non-public; then raise MAX_BRAND_REDIRECTS in a focused change with security review.
  3. Skip this asset and let the extraction continue with a fallback (logo/seed/imagery fallbacks all tolerate a missing asset).
  4. Investigate redirect loops — they are often a symptom of an auth wall or a broken CDN config, not a real asset.

Example fix

// before
const res = await fetchExternalBrandAsset(shortUrl); // >5 hops
// after — pre-resolve and fetch the terminal URL
const terminal = await resolveFinalUrl(shortUrl); // your own helper, also SSRF-checked
const res = await fetchExternalBrandAsset(terminal);
Defensive patterns

Strategy: fallback

Validate before calling

const MAX = 5; // keep in sync with safe-fetch MAX_BRAND_REDIRECTS
async function headResolve(url) {
  let target = url, hops = 0;
  while (hops++ <= MAX) {
    const res = await fetch(target, { method: 'HEAD', redirect: 'manual' });
    const loc = res.headers.get('location');
    if (!loc || res.status < 300 || res.status >= 400) return target;
    target = new URL(loc, target).toString();
  }
  return null;
}

Try / catch

try { return await fetchExternalBrandAsset(u); }
catch (e) {
  if (String(e.message).startsWith('too many brand asset redirects')) {
    return null; // fall back to a default asset
  }
  throw e;
}

Prevention

When it happens

Trigger: A brand asset URL whose chain of 3xx responses exceeds five hops — e.g. a CDN edge -> CDN origin -> auth gate -> login redirect -> another redirect -> another, all with Location headers. Also triggered by an accidental redirect loop that re-issues the same Location.

Common situations: Assets behind a sequence of shortener -> CDN -> auth -> final; a misconfigured server returning a redirect cycle; the original asset moved several times; an authenticated CDN that keeps bouncing to a login page.

Related errors


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