JuliusBrussee/caveman · error
timeoutMs must be a positive integer
Error message
timeoutMs must be a positive integer
What it means
The Cave constructor validates the optional timeoutMs option: if provided, it must be a safe integer strictly greater than zero. Zero, negative numbers, NaN, Infinity, and non-integers (floats, strings, big values beyond Number.MAX_SAFE_INTEGER) are rejected so timeout handling downstream can rely on a valid millisecond value.
Solutions
- Pass a positive integer of milliseconds, e.g. timeoutMs: 30000.
- To use no timeout, omit the option entirely (undefined is allowed) instead of passing 0.
- Coerce env/config values explicitly: `const t = Number(raw); if (Number.isSafeInteger(t) && t > 0) ...`.
Example fix
// before
new Cave({ apiKey, baseURL, agent: "x", timeoutMs: Number(process.env.TIMEOUT) });
// after
const t = Number(process.env.TIMEOUT);
new Cave({ apiKey, baseURL, agent: "x", ...(Number.isSafeInteger(t) && t > 0 ? { timeoutMs: t } : {}) }); Defensive patterns
Strategy: validation
Validate before calling
function parseTimeoutMs(raw: string | undefined): number | undefined {
if (raw === undefined || raw === "") return undefined;
const n = Number(raw);
if (!Number.isSafeInteger(n) || n <= 0) throw new Error(`timeoutMs must be a positive integer, got ${raw}`);
return n;
} Type guard
const isValidTimeoutMs = (v: unknown): v is number => typeof v === "number" && Number.isSafeInteger(v) && v > 0;
Try / catch
try {
const cave = new Cave(options);
} catch (e) {
if (e instanceof Error && e.message.includes("timeoutMs")) {
// drop the bad option and retry with the SDK default
const { timeoutMs, ...rest } = options;
const cave = new Cave(rest);
} else throw e;
} Prevention
- Omit timeoutMs entirely for default behavior; never use 0 to mean 'no timeout'.
- Coerce and validate env/config numbers with Number() plus Number.isSafeInteger checks.
- Keep the value in milliseconds consistently; document the unit where it's configured.
When it happens
Trigger: new Cave({ ..., timeoutMs: 0 }) (e.g. "disable timeout" attempt), timeoutMs: -1, timeoutMs: 1.5, timeoutMs: Number(env) producing NaN, or timeoutMs: "30000" (string).
Common situations: Using 0 intending 'no timeout'; reading the value from env/config where it stays a string or empty; seconds-vs-milliseconds confusion producing a fractional or wrong-scale value; JSON config containing null.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- apiKey, baseURL, and agent are required
- ASGI context resolver or request bounds are invalid
- bedrock endpoint must be runtime or mantle
- cave_mastra_max_steps_invalid
- cave_memory_agent_invalid
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/3f2a43c969502d39.
Report an issue: GitHub.
Appendix: source
Thrown at packages/sdk/typescript/src/index.ts:356
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();
const envWorkflow = envRaw && /^[a-z0-9_-]{1,96}$/.test(envRaw) ? envRaw : undefined;
const normalized: CaveOptions = {
...options,
baseURL: normalizedServiceURL(options.baseURL, "baseURL"),
...(options.controlURL === undefined
? {}
: { controlURL: normalizedServiceURL(options.controlURL, "controlURL") }),
};
this.options = !normalized.defaultWorkflow && envWorkflow
? { ...normalized, defaultWorkflow: envWorkflow }View on GitHub (pinned to 3ee70a1026)