withastro/astro · error · MultiLevelEncodingError
URL encoding depth exceeded the maximum number of decode ite
Error message
URL encoding depth exceeded the maximum number of decode iterations
What it means
validateAndDecodePathname() decodes a request pathname repeatedly until it stops changing (MAX_DECODE_ITERATIONS = 10), so middleware checks and routing always see the final, real path. MultiLevelEncodingError is thrown when the path is STILL changing after those 10 passes - it was encoded many times over (e.g. `%25252561`). Handing back a half-decoded path could let middleware authorize one path while a later decode turns it into a different, possibly protected, path, so the request is rejected with a 400 instead of guessing.
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 52e6c34790)
Solutions
- Encode exactly once, at the boundary that first builds the URL - remove extra encodeURIComponent/encodeURI layers.
- Audit middleware, proxies, and fetch wrappers between the client and Astro for re-encoding of already-encoded paths.
- If an external system legitimately forwards multiply-encoded paths, decode them fully before handing the path to Astro.
- If the producer cannot be changed, reject such requests at the edge (WAF/CDN rule) so they never reach routing.
Example fix
// before - value encoded twice
const url = `/docs/${encodeURIComponent(encodeURIComponent(path))}`;
// after - encode exactly once
const url = `/docs/${encodeURIComponent(path)}`; Defensive patterns
Strategy: validation
Validate before calling
const MAX_HOPS = 10; // mirrors Astro's decode limit
function isReasonablyEncoded(pathname: string): boolean {
let current = pathname;
for (let i = 0; i < MAX_HOPS; i++) {
let next: string;
try {
next = decodeURI(current);
} catch {
return true; // decoding stops here; nothing more will change
}
if (next === current) return true;
current = next;
}
return false; // still changing after 10 passes - Astro will reject it
} Prevention
- Apply encodeURIComponent exactly once per URL segment.
- Never re-encode an already-encoded value; decode it first if you must change it.
- Audit proxies and fetch wrappers in the request chain for re-encoding of paths.
- Include double-encoding probes in staging security scans so surprises surface before production.
When it happens
Trigger: A path encoded three or more times, e.g. `a` -> `%61` -> `%2561` -> `%252561`; middleware, a proxy, or a fetch wrapper that re-encodes an already-encoded path on every hop; crafted requests attempting to smuggle a path like /admin past middleware via layered encoding.
Common situations: Chained encodeURIComponent() calls applied to the same value on both client and server; API gateways or CDNs that normalize and re-encode path components; penetration tests reporting double/triple encoding as a path-traversal finding against the Astro app.
Related errors
- Invalid URL encoding
- MissingMiddlewareForInternationalization
- IncorrectStrategyForI18n
- InvalidI18nMiddlewareConfiguration
- RemoteImageNotAllowed
AI-assisted analysis of withastro/astro@52e6c34790 (2026-08-18).
Data as JSON: /api/errors/3267cdee2daf976c.
Report an issue: GitHub.