{"record":{"id":"416fae3071779d8a","repo":"withastro/astro","slug":"invalid-url-encoding","errorCode":null,"errorMessage":"Invalid URL encoding","messagePattern":"Invalid URL encoding","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/astro/src/core/util/pathname.ts","lineNumber":44,"sourceCode":" * encoded several times ends up as a single, final path. This stops someone\n * from sneaking a path like `/admin` past middleware by encoding it multiple\n * times — middleware always sees the real, decoded path.\n *\n * @param pathname - The path to decode\n * @returns The final, fully decoded path\n * @throws Error if the path has broken encoding that can't be decoded at all\n *   (for example a lone `%` that isn't followed by two hex digits)\n * @throws MultiLevelEncodingError if the path is still changing after\n *   {@link MAX_DECODE_ITERATIONS} tries (it was encoded too many times).\n *   Handing back a half-decoded path here would bring back the security hole\n *   this function exists to close.\n */\nexport function validateAndDecodePathname(pathname: string): string {\n\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);","sourceCodeStart":26,"sourceCodeEnd":62,"githubUrl":"https://github.com/withastro/astro/blob/52e6c34790cc8ac4e69e6135ace06049867e5c4a/packages/astro/src/core/util/pathname.ts#L26-L62","documentation":"Astro fully decodes every request pathname so that middleware and routing always operate on the same real path. validateAndDecodePathname() throws this plain Error when the very first decodeURI() fails - the path contains percent-encoding that cannot be decoded at all, such as a lone `%` not followed by two hex digits. The request cannot be mapped to a real path safely, so it is rejected.","triggerScenarios":"A request whose path contains a stray or truncated percent sign: /shop/50%off, /foo%.pdf, or /a%2 (incomplete escape); links built by concatenating raw user input into URLs without encodeURIComponent; crawlers and security scanners probing with malformed escapes.","commonSituations":"Marketing or affiliate links containing a literal percent (like '50% off') that were never encoded; a proxy or rewrite layer mangling paths and leaving a dangling `%`; fuzzing tools sending garbage percent sequences that surface as request failures in logs.","solutions":["Find the offending URL in the request/error logs and fix the producer: encode a literal `%` as `%25` (encodeURIComponent('50%off') yields '50%25off').","If the path originates from user input or an external system, validate or reject paths with broken escapes before they reach routing (edge rule or middleware that returns 400).","Audit any proxy/CDN rewrite rules in front of Astro so already-encoded paths pass through unchanged."],"exampleFix":"// before - raw value with a literal % ends up in the URL\n<a href={`/shop/${coupon}`}> // coupon = '50%off' -> /shop/50%off\n\n// after - encode dynamic path segments exactly once\n<a href={`/shop/${encodeURIComponent(coupon)}`}> // /shop/50%25off","handlingStrategy":"validation","validationCode":"function isDecodablePathname(pathname: string): boolean {\n  try {\n    decodeURI(pathname);\n    return true;\n  } catch {\n    return false;\n  }\n}\n// in middleware or an edge handler:\n// if (!isDecodablePathname(url.pathname)) return new Response('Bad Request', { status: 400 });","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always pass dynamic path segments through encodeURIComponent(), exactly once.","Never build URLs by concatenating raw user input.","Treat undecodable paths as 400 Bad Request at your edge (CDN rule or middleware) rather than letting them surface as server errors."],"tags":["url-encoding","routing","middleware","security"],"backgroundTag":"malformed-url-encoding","analyzedSha":"52e6c34790cc8ac4e69e6135ace06049867e5c4a","analyzedAt":"2026-08-18T18:48:03.901Z","contentChangedAt":"2026-08-18T18:48:03.901Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}