{"record":{"id":"054de31c15da5549","repo":"musistudio/claude-code-router","slug":"only-http-and-https-urls-can-be-opened-054de3","errorCode":null,"errorMessage":"Only http and https URLs can be opened.","messagePattern":"Only http and https URLs can be opened\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/ui/src/web-client-bridge.ts","lineNumber":102,"sourceCode":"  }\n}\n\nfunction noopSubscription(): () => void {\n  return () => undefined;\n}\n\nasync function selectPluginDirectory(): Promise<unknown> {\n  const directory = window.prompt(\"Plugin directory path\");\n  if (!directory?.trim()) {\n    return undefined;\n  }\n  return rpc(\"selectPluginDirectory\", [directory.trim()]);\n}\n\nfunction normalizeExternalHttpUrl(value: string): string {\n  const url = new URL(value.trim());\n  if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n    throw new Error(\"Only http and https URLs can be opened.\");\n  }\n  return url.toString();\n}\n\nconst webClientBridge: CcrApi = {\n  applyClaudeAppGateway: (config) => rpc(\"applyClaudeAppGateway\", [config]) as ReturnType<CcrApi[\"applyClaudeAppGateway\"]>,\n  applyProfile: () => rpc(\"applyProfile\") as ReturnType<CcrApi[\"applyProfile\"]>,\n  cancelBotGatewayQrLogin: (request) => rpc(\"cancelBotGatewayQrLogin\", [request]) as ReturnType<CcrApi[\"cancelBotGatewayQrLogin\"]>,\n  checkProviderConnectivity: (request) => rpc(\"checkProviderConnectivity\", [request]) as ReturnType<CcrApi[\"checkProviderConnectivity\"]>,\n  clearProxyNetworkCaptures: () => rpc(\"clearProxyNetworkCaptures\") as ReturnType<CcrApi[\"clearProxyNetworkCaptures\"]>,\n  closeBotGatewayQrWindow: (request) => rpc(\"closeBotGatewayQrWindow\", [request]) as ReturnType<CcrApi[\"closeBotGatewayQrWindow\"]>,\n  closeTray: () => Promise.resolve(),\n  detectProviderIcon: (request) => rpc(\"detectProviderIcon\", [request]) as ReturnType<CcrApi[\"detectProviderIcon\"]>,\n  exportData: () => rpc(\"exportData\") as ReturnType<CcrApi[\"exportData\"]>,\n  fetchProviderManifest: (request) => rpc(\"fetchProviderManifest\", [request]) as ReturnType<CcrApi[\"fetchProviderManifest\"]>,\n  getAgentAnalysis: (filter) => rpc(\"getAgentAnalysis\", [filter]) as ReturnType<CcrApi[\"getAgentAnalysis\"]>,\n  getAgentTracePayload: (request) => rpc(\"getAgentTracePayload\", [request]) as ReturnType<CcrApi[\"getAgentTracePayload\"]>,\n  getAppInfo: () => rpc(\"getAppInfo\") as ReturnType<CcrApi[\"getAppInfo\"]>,","sourceCodeStart":84,"sourceCodeEnd":120,"githubUrl":"https://github.com/musistudio/claude-code-router/blob/99f24806c6a2c660b16e53e95211c517448a6c90/packages/ui/src/web-client-bridge.ts#L84-L120","documentation":"Thrown by normalizeExternalHttpUrl when asked to open a URL whose scheme is neither http: nor https:. The function parses the value with the URL constructor and whitelists only web schemes, deliberately blocking file:, javascript:, and other protocols.","triggerScenarios":"Calling the open-URL bridge path with values like file:///home/user/doc.html, mailto:someone@example.com, about:blank, or a bare string that the URL constructor resolves to a non-http scheme.","commonSituations":"Passing local filesystem paths expecting them to open in a browser; user-supplied links from chat/config that use custom app schemes; Windows paths like C:\\file.txt that parse as a drive-letter scheme.","solutions":["Use a full http(s) URL, e.g. https://example.com/page","For local files, do not route them through this API — use the platform file-open mechanism instead","Sanitize user input and reject non-http schemes before calling the bridge"],"exampleFix":"// before\nopenUrl(\"file:///home/user/report.html\")\n\n// after\nopenUrl(\"https://example.com/report.html\")","handlingStrategy":"type-guard","validationCode":"try { const u = new URL(value); if (u.protocol !== \"http:\" && u.protocol !== \"https:\") throw new Error(\"blocked\"); } catch { /* reject before calling bridge */ }","typeGuard":"function isExternalHttpUrl(value: string): boolean {\n  try { const u = new URL(value.trim()); return u.protocol === \"http:\" || u.protocol === \"https:\"; }\n  catch { return false; }\n}","tryCatchPattern":"try { openExternal(url); } catch (e) { if (e instanceof Error && e.message === \"Only http and https URLs can be opened.\") return; throw e; }","preventionTips":["Whitelist http/https at the input boundary","Open local files with shell.openPath, not the browser bridge","Treat all user-supplied URLs as untrusted; validate scheme before use"],"tags":["url-validation","security","scheme","electron"],"backgroundTag":"unsupported-url-scheme","analyzedSha":"99f24806c6a2c660b16e53e95211c517448a6c90","analyzedAt":"2026-08-27T04:11:01.184Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}