{"record":{"id":"4a2f085c3e52e2ff","repo":"mihomo-party-org/clash-party","slug":"invalid-plugin-url","errorCode":null,"errorMessage":"Invalid plugin URL","messagePattern":"Invalid plugin URL","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/main/resolve/plugin/remote.ts","lineNumber":11,"sourceCode":"import { getAppConfig } from '../../config/app'\nimport { MAX_PLUGIN_FILE_BYTES } from './constants'\nimport { requestOnce } from './http-client'\nimport { createGuardedLookup, isForbiddenHost } from './net-guard'\n\nfunction parseDownloadUrl(url: string): URL {\n  let parsed: URL\n  try {\n    parsed = new URL(url)\n  } catch {\n    throw new Error('Invalid plugin URL')\n  }\n  if (parsed.protocol !== 'https:') throw new Error('Plugin URL must use https')\n  if (parsed.username || parsed.password) throw new Error('Plugin URL must not contain userinfo')\n  if (parsed.hash) throw new Error('Plugin URL must not contain a fragment')\n  if (isForbiddenHost(parsed.hostname)) throw new Error('Plugin URL must use a public host')\n  return parsed\n}\n\nexport async function fetchRemotePlugin(url: string): Promise<string> {\n  const parsed = parseDownloadUrl(url)\n  const { subscriptionTimeout = 30000, pluginUseProxy } = await getAppConfig()\n  let proxy: { host: string; port: number } | undefined\n  if (pluginUseProxy) {\n    const { getControledMihomoConfig } = await import('../../config/controledMihomo')\n    const { 'mixed-port': port = 7890 } = await getControledMihomoConfig()\n    proxy = { host: '127.0.0.1', port }\n  }\n","sourceCodeStart":1,"sourceCodeEnd":29,"githubUrl":"https://github.com/mihomo-party-org/clash-party/blob/911e090537acdf7c50bee1c3aebecc2ef119a8b5/src/main/resolve/plugin/remote.ts#L1-L29","documentation":"parseDownloadUrl wraps the standard URL constructor: any input that fails new URL(url) is rethrown as 'Invalid plugin URL'. This is a syntax-level rejection before the https/userinfo/fragment/host checks run.","triggerScenarios":"Passing a non-URL string to fetchRemotePlugin / parseDownloadUrl — e.g. 'myplugin/download', an empty string, a file path like '/opt/plugins/x.yaml', or a URL with illegal characters (unencoded spaces, stray brackets).","commonSituations":"User pastes a relative path or bare domain without scheme; clipboard copy dropped part of the URL; local file path given where an https download URL is required.","solutions":["Prepend the scheme if missing (e.g. turn 'example.com/p.yaml' into 'https://example.com/p.yaml') before calling.","Validate with new URL(url) in the caller and surface a user-friendly message.","Ensure special characters are URL-encoded (spaces -> %20).","Remember only absolute https URLs are accepted — local files are not valid download URLs."],"exampleFix":"// before\nawait fetchRemotePlugin('example.com/plugins/app.yaml') // throws\n// after\nawait fetchRemotePlugin('https://example.com/plugins/app.yaml')","handlingStrategy":"validation","validationCode":"let u: URL\ntry { u = new URL(input) } catch { throw new Error('enter a valid absolute URL') }\nif (!/^[a-z][a-z0-9+.-]*:\\/\\//i.test(input)) throw new Error('URL must include a scheme, e.g. https://')","typeGuard":"const isAbsoluteUrl = (s: string): boolean => {\n  try { new URL(s); return true } catch { return false }\n}","tryCatchPattern":"try {\n  await fetchRemotePlugin(input)\n} catch (e) {\n  if (e.message === 'Invalid plugin URL') {\n    showUrlInputError('Not a valid absolute URL')\n  } else throw e\n}","preventionTips":["Trim and scheme-check user-supplied URLs before calling","Require users to paste full absolute https URLs","URL-encode spaces and special characters","Reject local file paths at the input layer"],"tags":["url","validation","plugin","input"],"backgroundTag":"invalid-url","analyzedSha":"911e090537acdf7c50bee1c3aebecc2ef119a8b5","analyzedAt":"2026-08-30T13:00:49.174Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}