{"record":{"id":"371c66e7c4a7f1ad","repo":"denoland/deno","slug":"err-unescaped-characters","errorCode":"ERR_UNESCAPED_CHARACTERS","errorMessage":"Request path contains unescaped characters","messagePattern":"Request path contains unescaped characters","errorType":"validation","errorClass":"NodeTypeError","httpStatus":null,"severity":"error","filePath":"ext/node/polyfills/_http_client.js","lineNumber":508,"sourceCode":"  } else if (typeof agent.addRequest !== \"function\") {\n    throw new ERR_INVALID_ARG_TYPE(\n      \"options.agent\",\n      [\"Agent-like Object\", \"undefined\", \"false\"],\n      agent,\n    );\n  }\n  this.agent = agent;\n\n  const protocol = options.protocol || defaultAgent.protocol;\n  let expectedProtocol = defaultAgent.protocol;\n  if (this.agent?.protocol) {\n    expectedProtocol = this.agent.protocol;\n  }\n\n  if (options.path) {\n    const path = String(options.path);\n    if (INVALID_PATH_REGEX.test(path)) {\n      throw new ERR_UNESCAPED_CHARACTERS(\"Request path\");\n    }\n  }\n\n  if (protocol !== expectedProtocol) {\n    throw new ERR_INVALID_PROTOCOL(protocol, expectedProtocol);\n  }\n\n  const defaultPort = options.defaultPort ||\n    (this.agent?.defaultPort);\n\n  const optsWithoutSignal = { __proto__: null, ...options };\n\n  // The `_proxy*` fields are internal transport details set only by the proxy\n  // selection below. A caller must not be able to supply them directly: doing\n  // so would route the request through an arbitrary proxy while bypassing the\n  // target permission check that the proxy branch performs. Strip any that came\n  // in via `options` so only the values computed here are honored.\n  delete optsWithoutSignal._proxy;","sourceCodeStart":490,"sourceCodeEnd":526,"githubUrl":"https://github.com/denoland/deno/blob/89f33cbef296a2b287f323d42de54c871fa69c77/ext/node/polyfills/_http_client.js#L490-L526","documentation":"ClientRequest validates options.path against INVALID_PATH_REGEX = /[^\\u0021-\\u00ff]/, i.e. any character below '!' (\\u0021) or above \\u00ff is rejected. This catches spaces (\\u0020), control characters, and non-Latin-1 characters (CJK, emoji) that must be percent-encoded in a request target. ERR_UNESCAPED_CHARACTERS is thrown before the request is ever sent.","triggerScenarios":"http.request({ path: '/search?q=hello world' }) (raw space); path containing raw UTF-8 like '/tags/日本語'; path built from unencoded user input with newlines or tabs.","commonSituations":"Building query strings by string concatenation instead of URLSearchParams; localizing routes or passing user-typed search text straight into path; proxying requests whose original path was never normalized.","solutions":["Encode dynamic segments: '/search?q=' + encodeURIComponent(q)","Build the whole target with URLSearchParams and use url.pathname + url.search","For full-path rewrites, run encodeURI() once and verify no raw spaces/control chars remain"],"exampleFix":"// before\nconst req = http.request({ host: 'x.test', path: `/search?q=${term}` }); // term = 'hello world'\n\n// after\nconst qs = new URLSearchParams({ q: term }).toString();\nconst req = http.request({ host: 'x.test', path: `/search?${qs}` });","handlingStrategy":"validation","validationCode":"const INVALID_PATH = /[^\\u0021-\\u00ff]/;\nconst path = buildPath(params);\nif (INVALID_PATH.test(path)) throw new Error('path needs encoding');\nhttp.request({ host, path: encodeURI(path) });","typeGuard":"function isEscapedPath(p) { return typeof p === 'string' && !/[^\\u0021-\\u00ff]/.test(p); }","tryCatchPattern":"try { http.request({ host, path }); } catch (e) { if (e.code === 'ERR_UNESCAPED_CHARACTERS') { /* re-encode and retry once with encodeURI(path) */ } else throw e; }","preventionTips":["Build query strings with URLSearchParams, never concatenation","encodeURIComponent every dynamic path segment","Centralize request-target construction in one helper"],"tags":["http","url-encoding","client","validation","node-compat"],"backgroundTag":null,"analyzedSha":"89f33cbef296a2b287f323d42de54c871fa69c77","analyzedAt":"2026-08-16T07:54:21.310Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}