getJson<T>() fetch with AbortSignal timeout

TypeScript
export class HttpError extends Error {
  constructor(readonly status: number, readonly body: string, url: string) {
    super(`GET ${url} returned HTTP ${status}`);
    this.name = 'HttpError';
  }
}

/** Fetch JSON with a hard timeout, explicit status checks and content-type guard. */
export async function getJson<T>(url: string, timeoutMs = 5_000): Promise<T> {
  const response = await fetch(url, {
    headers: { accept: 'application/json' },
    signal: AbortSignal.timeout(timeoutMs),
  });
  if (!response.ok) {
    throw new HttpError(response.status, (await response.text()).slice(0, 500), url);
  }
  const contentType = response.headers.get('content-type') ?? '';
  if (!contentType.toLowerCase().includes('json')) {
    throw new Error(`Expected JSON from ${url}, received '${contentType || 'unknown'}'`);
  }
  return (await response.json()) as T;
}

// const user = await getJson<{ id: number }>(`https://example.com/users/${id}`, 3_000);