JuliusBrussee/caveman · error

must not contain a query or fragment

Error message

${name} must not contain a query or fragment

What it means

normalizedServiceURL rejects baseURL/controlURL values that contain a query string (?...) or fragment (#...). The SDK appends its own paths and parameters to the base, so a preset query or fragment would produce malformed request URLs; it requires a clean origin-plus-path base.

Solutions

  1. Strip the query and fragment, keeping only scheme://host[/path].
  2. If you need an env query param, pass it via the SDK's dedicated options or headers, not the base URL.
  3. Use new URL(value) and pass url.origin + url.pathname to sanitize.

Example fix

// before
new Cave({ apiKey, baseURL: "https://api.example.com/?env=prod", agent: "x" });

// after
new Cave({ apiKey, baseURL: "https://api.example.com", agent: "x" });
Defensive patterns

Strategy: validation

Validate before calling

function stripQueryAndFragment(v: string): string {
  const u = new URL(v);
  return u.origin + u.pathname; // drops search and hash
}

Type guard

const hasNoQueryOrFragment = (v: string): boolean => {
  try { const u = new URL(v); return u.search === "" && u.hash === ""; } catch { return false; }
};

Try / catch

try {
  const cave = new Cave({ apiKey, baseURL, agent });
} catch (e) {
  if (e instanceof Error && e.message.includes("query or fragment")) {
    const u = new URL(baseURL);
    baseURL = u.origin + u.pathname;
    // retry construction
  } else throw e;
}

Prevention

When it happens

Trigger: baseURL: "https://api.example.com/?env=prod" or "https://api.example.com/page#section" passed to new Cave(...) via normalized().

Common situations: Copying the browser URL of a console page (with query/fragment) instead of the API base; appending auth or env parameters to the base URL; templating mistakes inserting a trailing '#'.

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


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/4688bf82fc48396f. Report an issue: GitHub.

Appendix: source

Thrown at packages/sdk/typescript/src/index.ts:346

  kind: number;
  startTimeNs: number;
  endTimeNs: number;
  statusCode: number; // 0=unset, 1=ok, 2=error
  attributes: Record<string, string | number | boolean>;
};

function normalizedServiceURL(value: string, name: "baseURL" | "controlURL"): string {
  if (value.trim() !== value) throw new Error(`${name} must not contain surrounding whitespace`);
  let parsed: URL;
  try {
    parsed = new URL(value);
  } catch {
    throw new Error(`${name} must be an absolute http(s) URL`);
  }
  if ((parsed.protocol !== "http:" && parsed.protocol !== "https:") || !parsed.hostname || parsed.username || parsed.password) {
    throw new Error(`${name} must be an absolute http(s) URL without credentials`);
  }
  if (parsed.search || parsed.hash) throw new Error(`${name} must not contain a query or fragment`);
  return value.replace(/\/+$/, "");
}

export class Cave {
  readonly options: CaveOptions;

  constructor(options: CaveOptions) {
    if (!options.apiKey || !options.baseURL || !options.agent) throw new Error("apiKey, baseURL, and agent are required");
    if (options.timeoutMs !== undefined && (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0)) {
      throw new Error("timeoutMs must be a positive integer");
    }
    // CAVE_WORKFLOW lets a wrapper (`cave wrap --workflow x`) label every request
    // from an SDK app without a code change. An explicit option always wins.
    // globalThis lookup keeps the SDK browser-neutral (no node type dependency).
    // The env value is normalized to the gateway's label rule (lowercase
    // [a-z0-9_-], max 96) — an invalid ambient value is ignored rather than
    // 400-ing every request. Mirrors caveman_cloud (Python).
    const envRaw = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env?.["CAVE_WORKFLOW"]?.toLowerCase();

View on GitHub (pinned to 3ee70a1026)