paperclipai/paperclip · error

devUiUrl must use http or https protocol

Error message

devUiUrl must use http or https protocol

What it means

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.

Source

Thrown at server/src/routes/plugin-ui-static.ts:341

          } catch {
            res.status(400).json({ error: "Invalid file path" });
            return;
          }
          if (
            decodedPath.includes("://") ||
            decodedPath.startsWith("//") ||
            decodedPath.startsWith("\\\\")
          ) {
            res.status(400).json({ error: "Invalid file path" });
            return;
          }

          // Proxy the request to the dev server
          const targetUrl = new URL(rawFilePath, devUiUrl.endsWith("/") ? devUiUrl : devUiUrl + "/");

          // SSRF protection: only allow http/https and localhost targets for dev proxy
          if (targetUrl.protocol !== "http:" && targetUrl.protocol !== "https:") {
            res.status(400).json({ error: "devUiUrl must use http or https protocol" });
            return;
          }

          // Dev proxy is restricted to loopback addresses only.
          // Validate the *constructed* targetUrl hostname (not the base) to
          // catch any path-based override that slipped past the checks above.
          const devHost = targetUrl.hostname;
          const isLoopback =
            devHost === "localhost" ||
            devHost === "127.0.0.1" ||
            devHost === "::1" ||
            devHost === "[::1]";
          if (!isLoopback) {
            log.warn(
              { pluginId: plugin.id, devUiUrl, host: devHost },
              "plugin-ui-static: devUiUrl must target localhost, rejecting proxy",
            );
            res.status(400).json({ error: "devUiUrl must target localhost" });

View on GitHub (pinned to 120ae5428f)

Solutions

  1. Set devUiUrl to a fully qualified http(s) URL, e.g. 'http://localhost:5173/' (scheme is mandatory)
  2. Double-check for typos such as 'http:/localhost:5173' or trailing text after the port
  3. After updating the plugin config, re-request the asset — the config is read per request via registry.getConfig

Example fix

# plugin company config — before
{ "devUiUrl": "localhost:5173" }

# after
{ "devUiUrl": "http://localhost:5173/" }
Defensive patterns

Strategy: validation

Validate before calling

const isValidDevUiUrl = (u: string): boolean => {
  try {
    const parsed = new URL(u);
    return parsed.protocol === "http:" || parsed.protocol === "https:";
  } catch {
    return false;
  }
};
if (!isValidDevUiUrl(config.devUiUrl)) throw new Error("devUiUrl must be a full http(s) URL");

Type guard

const isHttpUrl = (u: string): u is `http${"s" | ""}://${string}` => {
  try { return ["http:", "https:"].includes(new URL(u).protocol); } catch { return false; }
};

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of paperclipai/paperclip@120ae5428f (2026-08-18). Data as JSON: /api/errors/9369c501e562c4dd. Report an issue: GitHub.