{"record":{"id":"9369c501e562c4dd","repo":"paperclipai/paperclip","slug":"devuiurl-must-use-http-or-https-protocol","errorCode":null,"errorMessage":"devUiUrl must use http or https protocol","messagePattern":"devUiUrl must use http or https protocol","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"server/src/routes/plugin-ui-static.ts","lineNumber":341,"sourceCode":"          } catch {\n            res.status(400).json({ error: \"Invalid file path\" });\n            return;\n          }\n          if (\n            decodedPath.includes(\"://\") ||\n            decodedPath.startsWith(\"//\") ||\n            decodedPath.startsWith(\"\\\\\\\\\")\n          ) {\n            res.status(400).json({ error: \"Invalid file path\" });\n            return;\n          }\n\n          // Proxy the request to the dev server\n          const targetUrl = new URL(rawFilePath, devUiUrl.endsWith(\"/\") ? devUiUrl : devUiUrl + \"/\");\n\n          // SSRF protection: only allow http/https and localhost targets for dev proxy\n          if (targetUrl.protocol !== \"http:\" && targetUrl.protocol !== \"https:\") {\n            res.status(400).json({ error: \"devUiUrl must use http or https protocol\" });\n            return;\n          }\n\n          // Dev proxy is restricted to loopback addresses only.\n          // Validate the *constructed* targetUrl hostname (not the base) to\n          // catch any path-based override that slipped past the checks above.\n          const devHost = targetUrl.hostname;\n          const isLoopback =\n            devHost === \"localhost\" ||\n            devHost === \"127.0.0.1\" ||\n            devHost === \"::1\" ||\n            devHost === \"[::1]\";\n          if (!isLoopback) {\n            log.warn(\n              { pluginId: plugin.id, devUiUrl, host: devHost },\n              \"plugin-ui-static: devUiUrl must target localhost, rejecting proxy\",\n            );\n            res.status(400).json({ error: \"devUiUrl must target localhost\" });","sourceCodeStart":323,"sourceCodeEnd":359,"githubUrl":"https://github.com/paperclipai/paperclip/blob/120ae5428fa29bee300bcf806491cd4d965fbb7c/server/src/routes/plugin-ui-static.ts#L323-L359","documentation":"Returned as HTTP 400 by the plugin UI dev proxy (server/src/routes/plugin-ui-static.ts:341). After constructing targetUrl = new URL(rawFilePath, devUiUrl), the route only proxies when the resulting protocol is http or https. Any other scheme (file:, ftp:, or a scheme-looking host like 'localhost:5173' which the URL parser reads as protocol 'localhost:') is rejected as SSRF protection.","triggerScenarios":"Company plugin config configJson.devUiUrl set to a non-http(s) value: 'file:///home/me/plugin/dist/ui/', 'ftp://...', or most commonly a scheme-less value like 'localhost:5173' or '127.0.0.1:5173' — new URL('localhost:5173/') parses 'localhost:' as the protocol, so the constructed URL is neither http nor https and the request 400s.","commonSituations":"Plugin authors configuring hot-reload per PLUGIN_SPEC §27.2 and forgetting the http:// scheme; copy-pasting a filesystem path instead of a dev-server URL; template config files with placeholder values; environment changes where the dev server URL was edited by hand.","solutions":["Set devUiUrl to a fully qualified http(s) URL, e.g. 'http://localhost:5173/' (scheme is mandatory)","Double-check for typos such as 'http:/localhost:5173' or trailing text after the port","After updating the plugin config, re-request the asset — the config is read per request via registry.getConfig"],"exampleFix":"# plugin company config — before\n{ \"devUiUrl\": \"localhost:5173\" }\n\n# after\n{ \"devUiUrl\": \"http://localhost:5173/\" }","handlingStrategy":"validation","validationCode":"const isValidDevUiUrl = (u: string): boolean => {\n  try {\n    const parsed = new URL(u);\n    return parsed.protocol === \"http:\" || parsed.protocol === \"https:\";\n  } catch {\n    return false;\n  }\n};\nif (!isValidDevUiUrl(config.devUiUrl)) throw new Error(\"devUiUrl must be a full http(s) URL\");","typeGuard":"const isHttpUrl = (u: string): u is `http${\"s\" | \"\"}://${string}` => {\n  try { return [\"http:\", \"https:\"].includes(new URL(u).protocol); } catch { return false; }\n};","tryCatchPattern":null,"preventionTips":["Always include the http:// scheme in devUiUrl ('localhost:5173' parses as protocol 'localhost:')","Validate devUiUrl with new URL() before saving company plugin config"],"tags":["plugins","dev-proxy","ssrf","url-validation","http-400","configuration"],"backgroundTag":"invalid-url-scheme","analyzedSha":"120ae5428fa29bee300bcf806491cd4d965fbb7c","analyzedAt":"2026-08-18T22:49:45.177Z","schemaVersion":2},"datasetVersion":"2026-08-22T04:17:13.399Z"}