paperclipai/paperclip · error
Invalid configured Paperclip API origin
Error message
Invalid configured Paperclip API origin
What it means
runnerApiUrl parses the configured Paperclip API origin with new URL(apiUrl) and enforces a strict origin policy: protocol must be http: or https:, and username, password, search (query), and hash (fragment) must all be empty. Any violation throws "Invalid configured Paperclip API origin". This prevents the runner from sending authenticated agent JWTs to a malformed or smuggling-prone URL.
Source
Thrown at server/src/services/native-runtime/runner-api-client.ts:93
if (operation.method === "DELETE" && target.some(id => activeIds.includes(id))) throw forbidden("The active runner task cannot delete itself");
}
}
const contentType = (input.contentType ?? "application/json").split(";", 1)[0].trim().toLowerCase();
if (input.body !== undefined && !input.files?.length) {
const requestSchema = operation.requestBody?.content?.[contentType]?.schema as { type?: string } | undefined;
if (contentType.includes("json") && requestSchema?.type === "object" && (!input.body || typeof input.body !== "object" || Array.isArray(input.body))) {
throw badRequest("This operation requires body to be a JSON object. Pass the object directly, not a JSON-encoded string.");
}
if (contentType.includes("json") && requestSchema?.type === "array" && !Array.isArray(input.body)) {
throw badRequest("This operation requires body to be a JSON array. Pass the array directly, not a JSON-encoded string.");
}
}
return { input, operation };
}
export function runnerApiUrl(operation: RunnerApiOperation, input: RunnerApiCall, context: RunnerApiContext, apiUrl: string): URL {
const origin = new URL(apiUrl);
if (!["http:", "https:"].includes(origin.protocol) || origin.username || origin.password || origin.search || origin.hash) throw new Error("Invalid configured Paperclip API origin");
const params = { ...input.pathParams };
if (operation.path.includes("{companyId}")) params.companyId ??= context.companyId;
const names = [...operation.path.matchAll(/\{([^}]+)\}/g)].map((match) => match[1]);
for (const name of Object.keys(params)) if (!names.includes(name)) throw badRequest(`Unknown path parameter: ${name}`);
const path = operation.path.replace(/\{([^}]+)\}/g, (_, name: string) => {
const value = params[name];
if (!value || value === "." || value === ".." || /[\\/\x00-\x1f]/.test(value) || /%[0-9a-f]{2}/i.test(value)) throw badRequest(`Invalid or missing path parameter: ${name}`);
return encodeURIComponent(value);
});
const url = new URL(path, origin.origin);
if (url.origin !== origin.origin || !url.pathname.startsWith("/api/")) throw badRequest("Invalid API path");
for (const [key, value] of Object.entries(input.query ?? {})) {
if (value === undefined || value === null) continue;
const parameter = operation.parameters.find((entry) => entry.in === "query" && entry.name === key);
const values = Array.isArray(value) ? value : [value];
if (values.some((entry) => !["string", "number", "boolean"].includes(typeof entry))) throw badRequest(`Query parameter ${key} must contain scalar values`);
if (Array.isArray(value) && parameter?.explode === false) url.searchParams.set(key, values.join(","));
else for (const entry of values) url.searchParams.append(key, String(entry));View on GitHub (pinned to 01ad858492)
Solutions
- Set PAPERCLIP_API_URL (or binding.apiUrl) to a bare origin: scheme + host + optional port only, e.g. https://api.example.com or http://localhost:3100.
- Remove any query string, fragment, or userinfo (user:pass@) from the configured URL.
- Use http: or https: only; fix the scheme if a relative or non-http URL was supplied.
- Validate the value at deploy time with a quick check: new URL(v) && ['http:','https:'].includes(new URL(v).protocol) && !new URL(v).search && !new URL(v).hash && !new URL(v).username && !new URL(v).password.
Example fix
// before PAPERCLIP_API_URL=https://api.example.com/?env=prod // after PAPERCLIP_API_URL=https://api.example.com
Defensive patterns
Strategy: validation
Validate before calling
function isValidApiOrigin(v: string | undefined): v is string {
if (!v) return false;
try {
const u = new URL(v);
return ["http:", "https:"].includes(u.protocol) && !u.username && !u.password && !u.search && !u.hash;
} catch { return false; }
}
if (!isValidApiOrigin(process.env.PAPERCLIP_API_URL)) throw new Error("Set PAPERCLIP_API_URL to a bare http(s) origin"); Type guard
function isBareHttpOrigin(u: URL): boolean {
return ["http:", "https:"].includes(u.protocol) && !u.username && !u.password && !u.search && !u.hash;
} Try / catch
try {
const url = runnerApiUrl(operation, input, context, apiUrl);
} catch (err) {
if (err instanceof Error && err.message === "Invalid configured Paperclip API origin") {
// correct PAPERCLIP_API_URL / binding.apiUrl to scheme://host[:port]
} else throw err;
} Prevention
- Store only the origin (scheme + host + port) in PAPERCLIP_API_URL — no path suffix, query, fragment, or credentials.
- Validate the URL at config-load/startup time with a strict origin check.
- Never paste connection strings with embedded user:pass into the API origin variable.
- Add the origin check to deployment smoke tests.
When it happens
Trigger: io.apiUrl (from binding.apiUrl ?? process.env.PAPERCLIP_API_URL) is something like "https://api.example.com/path?x=1", "https://user:pass@host", "ftp://host", or contains a trailing fragment — anything failing the protocol/credentials/query/hash checks.
Common situations: PAPERCLIP_API_URL set with a trailing path, query string, or '#'; credentials embedded in the URL from a copied connection string; wrong scheme (e.g. "localhost:3100" parsed as an unexpected protocol); misconfigured reverse-proxy base URL.
Understand the failure class
Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.
Related errors
- ${label} must be an integer between 1 and ${maximum}.
- PAPERCLIP_EVAL_MAX_CAMPAIGN_COST_USD must be a positive fini
- Runner live rotation requires a valid generated-at time
- --agent-name cannot be empty.
- Paperclip Runner currently supports Codex only with codexPer
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/d3a775bb18d7a773.
Report an issue: GitHub.