{"record":{"id":"7a9f3dd23bf74a5b","repo":"withastro/astro","slug":"multilevelencodingerror","errorCode":"MultiLevelEncodingError","errorMessage":"URL encoding depth exceeded the maximum number of decode iterations","messagePattern":"URL encoding depth exceeded the maximum number of decode iterations","errorType":"http","errorClass":"MultiLevelEncodingError","httpStatus":400,"severity":"error","filePath":"packages/astro/src/core/util/pathname.ts","lineNumber":58,"sourceCode":"\tlet decoded: string;\n\ttry {\n\t\tdecoded = decodeURI(pathname);\n\t} catch (_e) {\n\t\tthrow new Error('Invalid URL encoding');\n\t}\n\t// Keep decoding until the path stops changing. A path can be encoded more\n\t// than once (for example %2561 → %61 → a), and we want the final decoded\n\t// path so the rest of Astro — especially middleware security checks —\n\t// always sees the same real path, no matter how many times it was encoded.\n\tlet iterations = 0;\n\twhile (decoded !== pathname) {\n\t\t// The path is still changing after the maximum number of tries, so it\n\t\t// was encoded too many times for us to fully decode. Stop and reject\n\t\t// it: handing back a half-decoded path could let middleware check one\n\t\t// path while a later decode (during rewrite routing) turns it into a\n\t\t// different, possibly protected, path.\n\t\tif (iterations >= MAX_DECODE_ITERATIONS) {\n\t\t\tthrow new MultiLevelEncodingError();\n\t\t}\n\t\tpathname = decoded;\n\t\ttry {\n\t\t\tdecoded = decodeURI(pathname);\n\t\t} catch {\n\t\t\t// decodeURI throws when decoding leaves a real '%' next to\n\t\t\t// characters that look like broken encoding (for example '%?.pdf'\n\t\t\t// after decoding %25%3F). That's fine — we've decoded as far as we\n\t\t\t// can and the path won't change any further.\n\t\t\tbreak;\n\t\t}\n\t\titerations++;\n\t}\n\treturn decoded;\n}\n","sourceCodeStart":40,"sourceCodeEnd":74,"githubUrl":"https://github.com/withastro/astro/blob/d081033d5fe8e8a68c4bbbad4af9d2deb9c74bca/packages/astro/src/core/util/pathname.ts#L40-L74","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Identify the source producing the multiply-encoded URL (proxy, CDN, redirect chain, or client) and fix it to send a single-encoded path.","If the path legitimately must be deeply encoded, decode it before it reaches Astro and pass the resolved path instead.","Treat this as a security signal: log the offending request and audit middleware authorization checks for path-based bypass vulnerabilities.","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."],"exampleFix":"// before: proxy forwards the raw, multiply-encoded URL\nproxy_to_astro(req.url)\n\n// after: decode once at the proxy edge so Astro receives a single-encoded path\nproxy_to_astro(encodeURI(decodeUntilStable(req.url)))","handlingStrategy":"validation","validationCode":"function isSafeSingleEncoded(pathname) {\n  let cur = pathname;\n  for (let i = 0; i < 11; i++) {\n    let next;\n    try { next = decodeURI(cur); } catch { return true; }\n    if (next === cur) return true;\n    cur = next;\n  }\n  return false; // still changing after 10 decodes -> would throw\n}","typeGuard":"null","tryCatchPattern":"null","preventionTips":["Decode incoming paths once at your proxy/edge before forwarding to Astro.","Treat MultiLevelEncodingError as a security signal and log the offending request.","Do not raise MAX_DECODE_ITERATIONS to work around it; fix the source of re-encoding."],"tags":["security","routing","url-encoding","middleware","request"],"backgroundTag":null,"analyzedSha":"d081033d5fe8e8a68c4bbbad4af9d2deb9c74bca","analyzedAt":"2026-08-12T13:37:29.035Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}