{"record":{"id":"62dd31742da95f81","repo":"withastro/astro","slug":"bad-request-62dd31","errorCode":null,"errorMessage":"Bad request.","messagePattern":"Bad request\\.","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"packages/integrations/node/src/standalone.ts","lineNumber":62,"sourceCode":"\t\tserver,\n\t\tdone: server.closed(),\n\t};\n}\n\n// also used by server entrypoint\nexport function createStandaloneHandler(\n\tapp: BaseApp,\n\toptions: Options,\n\theadersMap: NodeAppHeadersJson | undefined,\n) {\n\tconst appHandler = createAppHandler(app, options);\n\tconst staticHandler = createStaticHandler(app, options, headersMap);\n\treturn (req: http.IncomingMessage, res: http.ServerResponse) => {\n\t\ttry {\n\t\t\t// validate request path\n\t\t\tdecodeURI(req.url!);\n\t\t} catch {\n\t\t\tres.writeHead(400);\n\t\t\tres.end('Bad request.');\n\t\t\treturn;\n\t\t}\n\t\tstaticHandler(req, res, () => appHandler(req, res));\n\t};\n}\n\n// also used by preview entrypoint\nexport function createServer(listener: http.RequestListener, host: string, port: number) {\n\tlet httpServer: http.Server | https.Server;\n\n\tif (process.env.SERVER_CERT_PATH && process.env.SERVER_KEY_PATH) {\n\t\thttpServer = https.createServer(\n\t\t\t{\n\t\t\t\tkey: fs.readFileSync(process.env.SERVER_KEY_PATH),\n\t\t\t\tcert: fs.readFileSync(process.env.SERVER_CERT_PATH),\n\t\t\t},\n\t\t\tlistener,","sourceCodeStart":44,"sourceCodeEnd":80,"githubUrl":"https://github.com/withastro/astro/blob/52e6c34790cc8ac4e69e6135ace06049867e5c4a/packages/integrations/node/src/standalone.ts#L44-L80","documentation":"The @astrojs/node standalone server calls `decodeURI(req.url)` before dispatching to validate the request path. `decodeURI` throws `URIError: URI malformed` when the URL contains an invalid percent-escape — a `%` not followed by two hex digits or a truncated multi-byte sequence — and the server converts that into `400 Bad request.`. This is almost always a client-side URL-building bug (a literal `%` that was never encoded), not a server fault.","triggerScenarios":"Requesting a path containing a raw percent like `/sale/100%`, an invalid escape like `/foo%zz`, or a truncated sequence like `/%E2%82`.","commonSituations":"Links built by string concatenation with unencoded `%`; user-typed URLs containing %; crawlers or log scanners hitting odd paths; migrating from another server that silently tolerated malformed percent-escapes.","solutions":["Find the offending URL: the access log line above shows the exact path with the bad escape","Encode literal `%` as `%25` wherever links are generated (`/sale/100%25`)","Build URLs with `encodeURIComponent` for dynamic segments or the `URL` class instead of concatenation","If you must accept such paths, put a sanitizing middleware in front of the standalone handler"],"exampleFix":"// before: literal % breaks decodeURI -> 400 Bad request.\n<a href={`/discount/${label}`}>50% off</a> // label = '50%'\n\n// after: encode dynamic segments\n<a href={`/discount/${encodeURIComponent(label)}`}>50% off</a>","handlingStrategy":"validation","validationCode":"// Client-side: fix any href whose path contains an unescaped %\nfunction safeHref(path: string): string {\n  const u = new URL(path, location.origin);\n  if (/%(?![0-9A-Fa-f]{2})/.test(u.pathname)) {\n    return u.pathname.replace(/%(?![0-9A-Fa-f]{2})/g, '%25') + u.search;\n  }\n  return path;\n}","typeGuard":"function isValidEncodedPath(pathname: string): boolean {\n  try { decodeURI(pathname); return true; } catch { return false; }\n}","tryCatchPattern":null,"preventionTips":["Always build dynamic URL segments with encodeURIComponent","Prefer the URL class over string concatenation for links","Never emit literal '%' in hrefs from templates — write %25"],"tags":["node-adapter","url-encoding","http-400"],"backgroundTag":"invalid-uri-encoding","analyzedSha":"52e6c34790cc8ac4e69e6135ace06049867e5c4a","analyzedAt":"2026-08-18T18:48:03.901Z","contentChangedAt":"2026-08-18T18:48:03.901Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}