{"record":{"id":"801f1524f6ec098a","repo":"remix-run/remix","slug":"mounts-keys-must-be-url-pathnames-without-query-st","errorCode":null,"errorMessage":"mounts keys must be URL pathnames without query strings, fragments, or encoded dot segments. Received \"${urlRoot}\".","messagePattern":"mounts keys must be URL pathnames without query strings, fragments, or encoded dot segments\\. Received \"(.+?)\"\\.","errorType":"validation","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"packages/assets/src/lib/routes.ts","lineNumber":132,"sourceCode":"\n  return {\n    fileRoot: resolveMountFileRoot(options.rootDir, fileRoot),\n    fileRootValue: fileRoot,\n    urlRoot: joinUrlPath(normalizeMountUrlRoot(options.basePath), normalizeMountUrlRoot(urlRoot)),\n    urlRootKey: urlRoot,\n  }\n}\n\nfunction normalizeMountUrlRoot(urlRoot: string): string {\n  let normalizedRoot = normalizePathname(urlRoot).replace(/\\/+$/, '') || '/'\n  let url = new URL(normalizedRoot, 'http://remix.run')\n\n  if (\n    url.search !== '' ||\n    url.hash !== '' ||\n    getUrlPathSegmentCount(url.pathname) !== getUrlPathSegmentCount(normalizedRoot)\n  ) {\n    throw new TypeError(\n      `mounts keys must be URL pathnames without query strings, fragments, or encoded dot segments. Received \"${urlRoot}\".`,\n    )\n  }\n\n  return url.pathname.replace(/\\/+$/, '') || '/'\n}\n\nfunction resolveMountFileRoot(rootDir: string, fileRoot: string): string {\n  let resolvedRoot = resolveFilePath(rootDir, fileRoot)\n\n  try {\n    resolvedRoot = normalizeFilePath(fs.realpathSync(resolvedRoot))\n  } catch (error) {\n    if (!isUnresolvedPathError(error, resolvedRoot)) throw error\n  }\n\n  return resolvedRoot.replace(/\\/+$/, '') || '/'\n}","sourceCodeStart":114,"sourceCodeEnd":150,"githubUrl":"https://github.com/remix-run/remix/blob/9696913134be3a4423513d2775f7b31d6917c049/packages/assets/src/lib/routes.ts#L114-L150","documentation":"mounts keys are URL pathnames only: no query strings, no fragments, and no encoded dot segments (like %2e%2e) that would change the path structure after normalization. normalizeMountUrlRoot parses the key and compares path segment counts before/after normalization to catch encoded traversal.","triggerScenarios":"Passing mounts keys like '/assets?v=1', '/assets#frag', or '/%2e%2e/assets' to configMounts.","commonSituations":"Copying URL patterns from link hrefs (including query/hash) into mounts config; attempting path traversal via encoded segments; keys assembled from user input without sanitization.","solutions":["Use a plain pathname key: '/assets'","Strip query and hash when deriving keys from URLs: new URL(u).pathname","Reject keys containing '%' encodings of '.' or '/' segments before passing"],"exampleFix":"// before\nmounts: { '/assets?version=2': 'public/assets' }\n// after\nmounts: { '/assets': 'public/assets' }","handlingStrategy":"validation","validationCode":"for (const key of Object.keys(mounts)) {\n  const u = new URL(`http://x${key}`)\n  if (u.search || u.hash || /%2e/i.test(key)) throw new Error('bad mounts key: ' + key)\n}","typeGuard":"function isValidMountKey(key: string): boolean {\n  const u = new URL(`http://x${key}`)\n  return u.search === '' && u.hash === '' && !/%2e/i.test(key)\n}","tryCatchPattern":null,"preventionTips":["Use pathname-only strings for mount keys","Sanitize user-sourced keys through new URL(...).pathname"],"tags":["config","routes","mounts","url","security"],"backgroundTag":"invalid-url","analyzedSha":"9696913134be3a4423513d2775f7b31d6917c049","analyzedAt":"2026-08-27T19:55:01.024Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}