{"record":{"id":"d3a775bb18d7a773","repo":"paperclipai/paperclip","slug":"invalid-configured-paperclip-api-origin","errorCode":null,"errorMessage":"Invalid configured Paperclip API origin","messagePattern":"Invalid configured Paperclip API origin","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/native-runtime/runner-api-client.ts","lineNumber":93,"sourceCode":"      if (operation.method === \"DELETE\" && target.some(id => activeIds.includes(id))) throw forbidden(\"The active runner task cannot delete itself\");\n    }\n  }\n  const contentType = (input.contentType ?? \"application/json\").split(\";\", 1)[0].trim().toLowerCase();\n  if (input.body !== undefined && !input.files?.length) {\n    const requestSchema = operation.requestBody?.content?.[contentType]?.schema as { type?: string } | undefined;\n    if (contentType.includes(\"json\") && requestSchema?.type === \"object\" && (!input.body || typeof input.body !== \"object\" || Array.isArray(input.body))) {\n      throw badRequest(\"This operation requires body to be a JSON object. Pass the object directly, not a JSON-encoded string.\");\n    }\n    if (contentType.includes(\"json\") && requestSchema?.type === \"array\" && !Array.isArray(input.body)) {\n      throw badRequest(\"This operation requires body to be a JSON array. Pass the array directly, not a JSON-encoded string.\");\n    }\n  }\n  return { input, operation };\n}\n\nexport function runnerApiUrl(operation: RunnerApiOperation, input: RunnerApiCall, context: RunnerApiContext, apiUrl: string): URL {\n  const origin = new URL(apiUrl);\n  if (![\"http:\", \"https:\"].includes(origin.protocol) || origin.username || origin.password || origin.search || origin.hash) throw new Error(\"Invalid configured Paperclip API origin\");\n  const params = { ...input.pathParams };\n  if (operation.path.includes(\"{companyId}\")) params.companyId ??= context.companyId;\n  const names = [...operation.path.matchAll(/\\{([^}]+)\\}/g)].map((match) => match[1]);\n  for (const name of Object.keys(params)) if (!names.includes(name)) throw badRequest(`Unknown path parameter: ${name}`);\n  const path = operation.path.replace(/\\{([^}]+)\\}/g, (_, name: string) => {\n    const value = params[name];\n    if (!value || value === \".\" || value === \"..\" || /[\\\\/\\x00-\\x1f]/.test(value) || /%[0-9a-f]{2}/i.test(value)) throw badRequest(`Invalid or missing path parameter: ${name}`);\n    return encodeURIComponent(value);\n  });\n  const url = new URL(path, origin.origin);\n  if (url.origin !== origin.origin || !url.pathname.startsWith(\"/api/\")) throw badRequest(\"Invalid API path\");\n  for (const [key, value] of Object.entries(input.query ?? {})) {\n    if (value === undefined || value === null) continue;\n    const parameter = operation.parameters.find((entry) => entry.in === \"query\" && entry.name === key);\n    const values = Array.isArray(value) ? value : [value];\n    if (values.some((entry) => ![\"string\", \"number\", \"boolean\"].includes(typeof entry))) throw badRequest(`Query parameter ${key} must contain scalar values`);\n    if (Array.isArray(value) && parameter?.explode === false) url.searchParams.set(key, values.join(\",\"));\n    else for (const entry of values) url.searchParams.append(key, String(entry));","sourceCodeStart":75,"sourceCodeEnd":111,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/native-runtime/runner-api-client.ts#L75-L111","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nPAPERCLIP_API_URL=https://api.example.com/?env=prod\n// after\nPAPERCLIP_API_URL=https://api.example.com","handlingStrategy":"validation","validationCode":"function isValidApiOrigin(v: string | undefined): v is string {\n  if (!v) return false;\n  try {\n    const u = new URL(v);\n    return [\"http:\", \"https:\"].includes(u.protocol) && !u.username && !u.password && !u.search && !u.hash;\n  } catch { return false; }\n}\nif (!isValidApiOrigin(process.env.PAPERCLIP_API_URL)) throw new Error(\"Set PAPERCLIP_API_URL to a bare http(s) origin\");","typeGuard":"function isBareHttpOrigin(u: URL): boolean {\n  return [\"http:\", \"https:\"].includes(u.protocol) && !u.username && !u.password && !u.search && !u.hash;\n}","tryCatchPattern":"try {\n  const url = runnerApiUrl(operation, input, context, apiUrl);\n} catch (err) {\n  if (err instanceof Error && err.message === \"Invalid configured Paperclip API origin\") {\n    // correct PAPERCLIP_API_URL / binding.apiUrl to scheme://host[:port]\n  } else throw err;\n}","preventionTips":["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."],"tags":["url","configuration","validation","native-runtime"],"backgroundTag":"invalid-url-format","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}