{"record":{"id":"eed310b1e2d0ecc5","repo":"aaif-goose/goose","slug":"external-acp-backend-url-must-be-the-base-url-befo","errorCode":null,"errorMessage":"External ACP backend URL must be the base URL before /acp","messagePattern":"External ACP backend URL must be the base URL before /acp","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"ui/desktop/src/acp/url.ts","lineNumber":55,"sourceCode":"\nexport function normalizeAcpHttpBaseUrl(rawBaseUrl: string): string {\n  const trimmed = rawBaseUrl.trim();\n  if (!trimmed) {\n    throw new Error('External ACP backend URL is required');\n  }\n\n  const url = new URL(trimmed);\n  if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n    throw new Error(`External ACP backend URL must use http: or https:, got ${url.protocol}`);\n  }\n\n  if (url.search || url.hash) {\n    throw new Error('External ACP backend URL must not include query parameters or fragments');\n  }\n\n  const pathname = url.pathname.replace(/\\/+$/, '');\n  if (pathname.endsWith('/acp')) {\n    throw new Error('External ACP backend URL must be the base URL before /acp');\n  }\n\n  return `${url.origin}${pathname}`;\n}\n\nfunction httpEndpointUrlFromHttpBase(rawBaseUrl: string, endpoint: 'status' | 'acp'): string {\n  const baseUrl = normalizeAcpHttpBaseUrl(rawBaseUrl);\n  const url = new URL(baseUrl);\n  url.pathname = `${url.pathname.replace(/\\/+$/, '')}/${endpoint}`;\n  return url.toString();\n}\n\nexport function statusHttpUrlFromHttpBase(rawBaseUrl: string): string {\n  return httpEndpointUrlFromHttpBase(rawBaseUrl, 'status');\n}\n\nexport function acpHttpUrlFromHttpBase(rawBaseUrl: string, token?: string): string {\n  const url = new URL(httpEndpointUrlFromHttpBase(rawBaseUrl, 'acp'));","sourceCodeStart":37,"sourceCodeEnd":73,"githubUrl":"https://github.com/aaif-goose/goose/blob/3810898a7447ec3299be72e223d3570a7aabf0ab/ui/desktop/src/acp/url.ts#L37-L73","documentation":"Thrown by normalizeAcpHttpBaseUrl when the URL path (after stripping trailing slashes) ends with '/acp'. The function's contract is to receive the base URL before the /acp segment — httpEndpointUrlFromHttpBase then appends '/status' or '/acp' itself. Users commonly paste the full ACP endpoint they see in logs (e.g. http://127.0.0.1:8080/acp), which would otherwise produce a broken /acp/acp.","triggerScenarios":"Entering 'http://host:8080/acp' or 'https://host/goose/acp/' as the external backend URL — the exact endpoint string the goose daemon prints or that getAcpUrl returns.","commonSituations":"Copying the WebSocket/ACP endpoint from goose serve output into the external-backend settings field; following instructions that show the full endpoint rather than the base.","solutions":["Remove the trailing '/acp' segment: enter http://host:8080 or https://host/goose.","Keep only scheme + host + port (+ base path if the backend is mounted under one)."],"exampleFix":"// before\nconst pathname = url.pathname.replace(/\\/+$/, '');\nif (pathname.endsWith('/acp')) {\n  throw new Error('External ACP backend URL must be the base URL before /acp');\n}\n\n// after (accept the pasted endpoint by stripping the suffix)\nlet pathname = url.pathname.replace(/\\/+$/, '');\nif (pathname.endsWith('/acp')) {\n  pathname = pathname.slice(0, -'/acp'.length);\n}","handlingStrategy":"validation","validationCode":"// Strip an accidental /acp suffix before validating\nfunction stripAcpSuffix(raw: string): string {\n  const url = new URL(raw.trim());\n  url.pathname = url.pathname.replace(/\\/+$/, '').replace(/\\/acp$/, '');\n  return url.toString();\n}","typeGuard":"function isBaseUrlBeforeAcp(value: string): boolean {\n  try {\n    const url = new URL(value.trim());\n    return !url.pathname.replace(/\\/+$/, '').endsWith('/acp');\n  } catch {\n    return false;\n  }\n}","tryCatchPattern":"try {\n  const base = normalizeAcpHttpBaseUrl(inputUrl);\n} catch (error) {\n  if (/before \\/acp/.test(String(error))) {\n    return normalizeAcpHttpBaseUrl(stripAcpSuffix(inputUrl)); // user pasted the full endpoint\n  }\n  throw error;\n}","preventionTips":["Enter scheme + host + port only; the client appends /acp and /status.","Clarify with placeholder text: 'http://localhost:8080 (no /acp)'.","Auto-strip the suffix at the input boundary for pasted endpoint URLs."],"tags":["validation","url","endpoint","configuration"],"backgroundTag":null,"analyzedSha":"3810898a7447ec3299be72e223d3570a7aabf0ab","analyzedAt":"2026-08-16T10:14:26.282Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}