aaif-goose/goose · error

External ACP backend URL must be the base URL before /acp

Error message

External ACP backend URL must be the base URL before /acp

What it means

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.

Source

Thrown at ui/desktop/src/acp/url.ts:55

export function normalizeAcpHttpBaseUrl(rawBaseUrl: string): string {
  const trimmed = rawBaseUrl.trim();
  if (!trimmed) {
    throw new Error('External ACP backend URL is required');
  }

  const url = new URL(trimmed);
  if (url.protocol !== 'http:' && url.protocol !== 'https:') {
    throw new Error(`External ACP backend URL must use http: or https:, got ${url.protocol}`);
  }

  if (url.search || url.hash) {
    throw new Error('External ACP backend URL must not include query parameters or fragments');
  }

  const pathname = url.pathname.replace(/\/+$/, '');
  if (pathname.endsWith('/acp')) {
    throw new Error('External ACP backend URL must be the base URL before /acp');
  }

  return `${url.origin}${pathname}`;
}

function httpEndpointUrlFromHttpBase(rawBaseUrl: string, endpoint: 'status' | 'acp'): string {
  const baseUrl = normalizeAcpHttpBaseUrl(rawBaseUrl);
  const url = new URL(baseUrl);
  url.pathname = `${url.pathname.replace(/\/+$/, '')}/${endpoint}`;
  return url.toString();
}

export function statusHttpUrlFromHttpBase(rawBaseUrl: string): string {
  return httpEndpointUrlFromHttpBase(rawBaseUrl, 'status');
}

export function acpHttpUrlFromHttpBase(rawBaseUrl: string, token?: string): string {
  const url = new URL(httpEndpointUrlFromHttpBase(rawBaseUrl, 'acp'));

View on GitHub (pinned to 3810898a74)

Solutions

  1. Remove the trailing '/acp' segment: enter http://host:8080 or https://host/goose.
  2. Keep only scheme + host + port (+ base path if the backend is mounted under one).

Example fix

// before
const pathname = url.pathname.replace(/\/+$/, '');
if (pathname.endsWith('/acp')) {
  throw new Error('External ACP backend URL must be the base URL before /acp');
}

// after (accept the pasted endpoint by stripping the suffix)
let pathname = url.pathname.replace(/\/+$/, '');
if (pathname.endsWith('/acp')) {
  pathname = pathname.slice(0, -'/acp'.length);
}
Defensive patterns

Strategy: validation

Validate before calling

// Strip an accidental /acp suffix before validating
function stripAcpSuffix(raw: string): string {
  const url = new URL(raw.trim());
  url.pathname = url.pathname.replace(/\/+$/, '').replace(/\/acp$/, '');
  return url.toString();
}

Type guard

function isBaseUrlBeforeAcp(value: string): boolean {
  try {
    const url = new URL(value.trim());
    return !url.pathname.replace(/\/+$/, '').endsWith('/acp');
  } catch {
    return false;
  }
}

Try / catch

try {
  const base = normalizeAcpHttpBaseUrl(inputUrl);
} catch (error) {
  if (/before \/acp/.test(String(error))) {
    return normalizeAcpHttpBaseUrl(stripAcpSuffix(inputUrl)); // user pasted the full endpoint
  }
  throw error;
}

Prevention

When it happens

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

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

Related errors


AI-assisted analysis of aaif-goose/goose@3810898a74 (2026-08-16). Data as JSON: /api/errors/eed310b1e2d0ecc5. Report an issue: GitHub.