withastro/astro · error · MultiLevelEncodingError

MultiLevelEncodingError

MultiLevelEncodingError

Error message

URL encoding depth exceeded the maximum number of decode iterations

What it means

Astro repeatedly runs decodeURI on an incoming request pathname until it stops changing, so middleware and routing always see the real path no matter how many times it was percent-encoded. The loop is capped at 10 iterations (MAX_DECODE_ITERATIONS). If the path is still mutating after 10 passes, the decoder rejects it outright rather than risk handing a half-decoded path to middleware that could let a later decode reveal a different (possibly protected) route.

Source

Thrown at packages/astro/src/core/util/pathname.ts:58

	let decoded: string;
	try {
		decoded = decodeURI(pathname);
	} catch (_e) {
		throw new Error('Invalid URL encoding');
	}
	// Keep decoding until the path stops changing. A path can be encoded more
	// than once (for example %2561 → %61 → a), and we want the final decoded
	// path so the rest of Astro — especially middleware security checks —
	// always sees the same real path, no matter how many times it was encoded.
	let iterations = 0;
	while (decoded !== pathname) {
		// The path is still changing after the maximum number of tries, so it
		// was encoded too many times for us to fully decode. Stop and reject
		// it: handing back a half-decoded path could let middleware check one
		// path while a later decode (during rewrite routing) turns it into a
		// different, possibly protected, path.
		if (iterations >= MAX_DECODE_ITERATIONS) {
			throw new MultiLevelEncodingError();
		}
		pathname = decoded;
		try {
			decoded = decodeURI(pathname);
		} catch {
			// decodeURI throws when decoding leaves a real '%' next to
			// characters that look like broken encoding (for example '%?.pdf'
			// after decoding %25%3F). That's fine — we've decoded as far as we
			// can and the path won't change any further.
			break;
		}
		iterations++;
	}
	return decoded;
}

View on GitHub (pinned to d081033d5f)

Solutions

  1. Identify the source producing the multiply-encoded URL (proxy, CDN, redirect chain, or client) and fix it to send a single-encoded path.
  2. If the path legitimately must be deeply encoded, decode it before it reaches Astro and pass the resolved path instead.
  3. Treat this as a security signal: log the offending request and audit middleware authorization checks for path-based bypass vulnerabilities.
  4. Do not attempt to raise MAX_DECODE_ITERATIONS as a workaround; the cap exists to prevent half-decoded paths from diverging from middleware's checked path.

Example fix

// before: proxy forwards the raw, multiply-encoded URL
proxy_to_astro(req.url)

// after: decode once at the proxy edge so Astro receives a single-encoded path
proxy_to_astro(encodeURI(decodeUntilStable(req.url)))
Defensive patterns

Strategy: validation

Validate before calling

function isSafeSingleEncoded(pathname) {
  let cur = pathname;
  for (let i = 0; i < 11; i++) {
    let next;
    try { next = decodeURI(cur); } catch { return true; }
    if (next === cur) return true;
    cur = next;
  }
  return false; // still changing after 10 decodes -> would throw
}

Type guard

null

Try / catch

null

Prevention

When it happens

Trigger: A client request whose pathname is percent-encoded more than 10 times deep (e.g. 'a' encoded 11 times as %2525...2561), or a path that oscillates/keeps producing new '%' sequences on each decodeURI pass so the loop never converges within 10 iterations.

Common situations: Adversarial or fuzzed URLs probing for path-traversal bypasses; misconfigured reverse proxies or CDNs that re-encode already-encoded paths in a loop; bots scanning for middleware-auth-bypass via double/triple encoding that accidentally exceed the cap.

Related errors


AI-assisted analysis of withastro/astro@d081033d5f (2026-08-12). Data as JSON: /api/errors/7a9f3dd23bf74a5b. Report an issue: GitHub.