Hmbown/CodeWhale · error · ApiError

returned HTTP .

Error message

${label} returned HTTP ${statusCode}.

What it means

The VS Code extension's API client routes every HTTP call through ensureOk, which throws an ApiError whenever the runtime answers a non-2xx status. The message embeds the route label and the numeric status; response-body detail is attached to the ApiError. It is a uniform guard, so any failed REST call to the Codewhale runtime surfaces here.

Solutions

  1. Check that the Codewhale runtime is running and reachable at the configured base URL and restart it if needed
  2. Read the ApiError's statusCode and detail body to identify the route-specific cause (404: thread missing; 409: turn conflict; 5xx: server fault)
  3. Refresh the thread list and retry against a valid thread key if you got 404
  4. Retry the failed operation after the runtime recovers — for 5xx/502/503 this is usually transient

Example fix

// before: calling with a stale thread key after runtime reset
const detail = await api.getThreadDetail(oldKey); // 404 -> ApiError

// after: resolve a current key first
const threads = await api.listThreadSummaries();
const detail = threads.length ? await api.getThreadDetail(threads[0].key) : null;
Defensive patterns

Strategy: try-catch

Validate before calling

const healthy = await fetch(`${baseUrl}/health`).then(r => r.ok, () => false);
if (!healthy) throw new Error('Codewhale runtime unreachable - start it before using the extension');

Type guard

const isApiError = (e) => e instanceof ApiError || (e && typeof e.statusCode === 'number');

Try / catch

try {
  await api.startTurn(threadKey, input);
} catch (e) {
  if (isApiError(e)) {
    if (e.statusCode === 404) { /* refresh thread list, pick a valid key */ }
    else if (e.statusCode >= 500) { /* retry after runtime recovers */ }
    else throw e;
  } else throw e;
}

Prevention

When it happens

Trigger: Any of listThreadSummaries, getThreadDetail, createThread, startTurn, steerTurn, or interruptTurn receiving a response whose statusCode fails isOk (outside 2xx) — e.g. the runtime is down (connection refused yields its own failure path, but 404/409/500 bodies come through here), the thread key is unknown (404), or the runtime returns 500.

Common situations: Runtime server not running or restarted while the extension was open; stale thread keys after a data reset (404 on getThreadDetail); concurrent turns causing 409 on startTurn/steerTurn; auth or version mismatch producing 4xx; proxy/firewall returning an HTML error page with 502/503.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/5ef411aa9eddee91. Report an issue: GitHub.

Appendix: source

Thrown at extensions/vscode/src/api.ts:423

/** Build the typed error for a non-2xx status, surfacing the runtime's own message. */
function apiError(statusCode: number, body: unknown, label: string): ApiError {
  const detail = readErrorDetail(body);
  if (statusCode === 0) {
    return new ApiError(`${label} could not reach the runtime.`, 0, detail);
  }
  if (statusCode === 401) {
    return new ApiError(`${label} requires the runtime token.`, 401, detail);
  }
  if (statusCode === 409) {
    return new ConflictError(`${label} conflicts with the runtime's current state.`, detail);
  }
  return new ApiError(`${label} returned HTTP ${statusCode}.`, statusCode, detail);
}

/** Throw unless the runtime answered 2xx. Every route's status check runs through here. */
function ensureOk(response: RequestResult, label: string): void {
  if (!isOk(response.statusCode)) {
    throw apiError(response.statusCode, response.body, label);
  }
}

async function requestJson(
  url: string,
  config: ApiConfig,
  options: { method?: string; body?: string; timeoutMs: number },
): Promise<RequestResult> {
  try {
    return await new Promise<RequestResult>((resolve, reject) => {
      const request = http.request(
        url,
        {
          method: options.method ?? "GET",
          timeout: options.timeoutMs,
          headers: {
            Accept: "application/json",
            ...(options.body ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(options.body) } : {}),

View on GitHub (pinned to 433685b202)