paperclipai/paperclip · error
devUiUrl must target localhost
Error message
devUiUrl must target localhost
What it means
Returned as HTTP 400 by the plugin UI dev proxy (server/src/routes/plugin-ui-static.ts:359). Even with a valid http/https URL, the proxy only forwards to literal loopback hostnames: 'localhost', '127.0.0.1', '::1', or '[::1]'. The check runs on the *constructed* target URL hostname, so both a non-loopback devUiUrl and any path trick that changes the hostname are rejected.
Source
Thrown at server/src/routes/plugin-ui-static.ts:359
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" });
return;
}
log.debug(
{ pluginId: plugin.id, devUiUrl, targetUrl: targetUrl.href },
"plugin-ui-static: proxying to devUiUrl",
);
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10_000);
try {
const upstream = await fetch(targetUrl.href, { signal: controller.signal });
if (!upstream.ok) {
res.status(upstream.status).json({
error: `Dev server returned ${upstream.status}`,
});
return;View on GitHub (pinned to 120ae5428f)
Solutions
- Change devUiUrl to 'http://localhost:<port>/' or 'http://127.0.0.1:<port>/' — these are the only accepted hostnames
- Make sure the dev server actually listens on loopback (vite dev --host localhost or default behavior)
- If the dev server runs in a container or another host, port-forward it to loopback (e.g. kubectl port-forward, ssh -L) and point devUiUrl at the local forwarded port
Example fix
# before
{ "devUiUrl": "http://0.0.0.0:5173/" }
# after
{ "devUiUrl": "http://127.0.0.1:5173/" } Defensive patterns
Strategy: validation
Validate before calling
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
const isLoopbackDevUiUrl = (u: string): boolean => {
try { return LOOPBACK_HOSTS.has(new URL(u).hostname); } catch { return false; }
}; Type guard
const isLoopbackUrl = (u: string): boolean => {
try { return ["localhost", "127.0.0.1", "::1", "[::1]"].includes(new URL(u).hostname); } catch { return false; }
}; Prevention
- Use http://localhost:<port> or http://127.0.0.1:<port> — '0.0.0.0' and LAN IPs are rejected
- Port-forward remote/containerized dev servers to loopback instead of pointing devUiUrl at them directly
When it happens
Trigger: devUiUrl set to 'http://0.0.0.0:5173' (hostname '0.0.0.0' is not in the allowlist), 'http://192.168.1.20:5173', or an /etc/hosts alias like 'http://myplugin.localhost:5173' — none match the literal loopback strings, so any proxied asset request 400s. Also fires when the requested file path itself is crafted to change the hostname.
Common situations: Dev server bound to all interfaces and reached via the machine's LAN IP; using 0.0.0.0 as a 'same machine' shorthand; hostnames like *.localhost that browsers resolve but this literal comparison rejects; containerized setups where the plugin dev server is on another network namespace.
Related errors
- devUiUrl must use http or https protocol
- Invalid file path
- Invalid status '${String(rawStatus)}'. Must be one of: ${PLU
- Plugin tool dispatch is not enabled
- Request body is required
AI-assisted analysis of paperclipai/paperclip@120ae5428f (2026-08-18).
Data as JSON: /api/errors/c9d0a1ef7622a422.
Report an issue: GitHub.