can1357/oh-my-pi · error · ApiError

HTTP error ${res.status} on ${endpoint}

Error message

HTTP error ${res.status} on ${endpoint}

What it means

fetchJson() in the omp stats dashboard client wraps every REST call and throws ApiError when the HTTP response status is not ok (anything outside 2xx). The error carries the status code and the endpoint so the UI layer can distinguish server failures from bad requests. It applies to all stats fetches: overview, dashboards, recent requests/errors, and request details.

Source

Thrown at packages/stats/src/client/api.ts:32

const API_BASE = "/api";

export class ApiError extends Error {
	status: number;
	endpoint: string;

	constructor(status: number, endpoint: string, message: string) {
		super(message);
		this.name = "ApiError";
		this.status = status;
		this.endpoint = endpoint;
	}
}

async function fetchJson<T>(endpoint: string, options?: RequestInit): Promise<T> {
	const res = await fetch(endpoint, options);
	if (!res.ok) {
		throw new ApiError(res.status, endpoint, `HTTP error ${res.status} on ${endpoint}`);
	}
	return res.json() as Promise<T>;
}

export async function getOverviewStats(range: TimeRange = "24h", signal?: AbortSignal): Promise<OverviewStats> {
	return fetchJson<OverviewStats>(`${API_BASE}/stats/overview?range=${encodeURIComponent(range)}`, {
		signal,
	});
}

export async function getModelDashboardStats(
	range: TimeRange = "24h",
	signal?: AbortSignal,
): Promise<ModelDashboardStats> {
	return fetchJson<ModelDashboardStats>(`${API_BASE}/stats/model-dashboard?range=${encodeURIComponent(range)}`, {
		signal,
	});
}

View on GitHub (pinned to 9690622007)

Solutions

  1. Check the stats server is running and reachable at API_BASE (curl the endpoint to see the real status/body)
  2. Retry with backoff if the server was booting (transient 502/503)
  3. Verify API_BASE and endpoint paths match the running server version
  4. Inspect the ApiError.status to branch: 404 → version mismatch/wrong base URL; 5xx → server-side failure; 4xx → bad parameters

Example fix

// before
const stats = await getOverviewStats(range); // throws ApiError on 500
// after
try {
  const stats = await getOverviewStats(range);
} catch (err) {
  if (err instanceof ApiError && (err.status === 502 || err.status === 503)) {
    stats = await withRetry(() => getOverviewStats(range));
  } else throw err;
}
Defensive patterns

Strategy: retry

Validate before calling

const res = await fetch(`${API_BASE}/stats/overview?range=${range}`, { method: 'HEAD' });
if (!res.ok) console.warn(`Stats API unhealthy: ${res.status}`);

Type guard

function isApiError(err: unknown): err is ApiError {
  return err instanceof ApiError;
}

Try / catch

try {
  const stats = await getOverviewStats(range, signal);
} catch (err) {
  if (err instanceof ApiError && err.status >= 500) {
    // transient — retry with backoff
  } else if (err instanceof ApiError && err.status === 404) {
    // version/base-URL mismatch — surface a config hint
  } else throw err;
}

Prevention

When it happens

Trigger: Any exported fetcher (getOverviewStats, getModelDashboardStats, getCostDashboardStats, getRecentRequests, getRecentErrors, getRequestDetails) receiving a 4xx/5xx response — server not running or crashed, wrong API_BASE, range parameter rejected, endpoint changed between versions, or a proxy returning 404/502.

Common situations: The omp stats server was restarted or is still booting while the dashboard polls; hitting an old cached bundle pointed at a removed endpoint; a reverse proxy answering 502 because the upstream stats server died; invalid TimeRange value producing a 400.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/46a7691a8c0a54a4. Report an issue: GitHub.