affaan-m/ECC · error · Error

plan-canvas request path must stay on the loopback server

Error message

plan-canvas request path must stay on the loopback server

What it means

After parsing the request path as a URL against the loopback base, validateRequestPath confirms the resolved hostname equals DEFAULT_HOST. A path like '//evil.example.com/x' resolves to a different host, so the guard rejects any attempt to redirect the request off the local plan-canvas server (SSRF-style protection).

Solutions

  1. Use a root-relative path only, e.g. '/api/sessions'.
  2. Never embed a host in the path argument; the host/port are fixed by requestOptions.
  3. Sanitize any externally sourced segment before interpolation (strip leading '//').
  4. If a different server is genuinely needed, change the server config, not the request path.

Example fix

// before
await request(port, 'GET', '//evil.example.com/api/sessions');
// after
await request(port, 'GET', '/api/sessions');
Defensive patterns

Strategy: validation

Validate before calling

function assertLoopbackPath(p) {
  const url = new URL(p, 'http://127.0.0.1');
  if (url.hostname !== '127.0.0.1') throw new Error(`path escapes loopback server: ${p}`);
  return url.pathname + url.search;
}

Type guard

function staysOnLoopback(v) { try { return new URL(v, 'http://127.0.0.1').hostname === '127.0.0.1'; } catch { return false; } }

Try / catch

try {
  const res = await request(port, method, requestPath);
} catch (err) {
  if (err.message.includes('must stay on the loopback server')) {
    console.error('Strip any host from the path; only root-relative paths are allowed.');
    process.exit(2);
  }
  throw err;
}

Prevention

When it happens

Trigger: Passing a path beginning with '//' or containing a full absolute URL ('http://otherhost/api/sessions') to the request helper — `new URL(requestPath, 'http://127.0.0.1')` then yields a foreign hostname.

Common situations: Concatenating user- or config-supplied strings into request paths; protocol-relative URLs sneaking in from template strings; security probing of the local server surface.

Understand the failure class

Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/219e189bbda74eb5. Report an issue: GitHub.

Appendix: source

Thrown at scripts/plan-canvas.js:107

    return null;
  }
}

function validatePort(port) {
  const value = Number(port);
  if (!Number.isInteger(value) || value < 0 || value > 65535) {
    throw new Error(`invalid plan-canvas server port: ${port}`);
  }
  return value;
}

function validateRequestPath(requestPath) {
  if (typeof requestPath !== 'string' || !requestPath.startsWith('/')) {
    throw new Error('plan-canvas request path must be root-relative');
  }
  const url = new URL(requestPath, `http://${DEFAULT_HOST}`);
  if (url.hostname !== DEFAULT_HOST) {
    throw new Error('plan-canvas request path must stay on the loopback server');
  }
  if (!SAFE_REQUEST_PATHS.has(url.pathname) && !SESSION_REPLY_PATH.test(url.pathname)) {
    throw new Error(`unsupported plan-canvas request path: ${url.pathname}`);
  }
  return `${url.pathname}${url.search}`;
}

function requestOptions(port, method, requestPath, headers) {
  return {
    host: DEFAULT_HOST,
    port: validatePort(port),
    method,
    path: validateRequestPath(requestPath),
    agent: false,
    headers
  };
}

View on GitHub (pinned to 8321021c54)