ramensoftware/windhawk · error · HttpError

Request failed with status

Error message

Request failed with status ${status}${statusText ? ` ${statusText}` : ''}

What it means

assertOk() wraps the fetch Response in swrHelpers and throws an HttpError carrying the numeric status and statusText whenever response.ok is false. All helpers (fetchText, fetchJson, fetchDefaultCatalog, fetchCatalogJson) funnel through it, so any non-2xx HTTP response becomes this error.

Solutions

  1. Log error.status to identify the exact HTTP failure and fix the URL/request accordingly
  2. Verify the endpoint is reachable (curl the URL) and the server is healthy
  3. Add retry/fallback logic around catalog fetches for transient 5xx

Example fix

// before
const text = await fetchText(url);
// after
try {
  const text = await fetchText(url);
} catch (e) {
  if (e instanceof HttpError && e.status === 404) useFallbackCatalog();
  else throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

const res = await fetch(url, { method: 'HEAD' });
if (!res.ok) throw new Error(`catalog endpoint unhealthy: ${res.status}`);

Type guard

class HttpError extends Error { constructor(public status: number, statusText: string) { super(statusText); } }
const isHttpError = (e: unknown): e is HttpError => e instanceof HttpError;

Try / catch

try { const data = await fetchJson(url); } catch (e) { if (isHttpError(e)) { if (e.status >= 500) retryLater(); else reportBadUrl(e.status); } else throw e; }

Prevention

When it happens

Trigger: fetchText/fetchJson/fetchDefaultCatalog/fetchCatalogJson called against a URL returning 404/500/401 etc., e.g. the catalog JSON endpoint is down or the path moved.

Common situations: Catalog server offline, wrong base URL, expired auth, CDN 404 after a release renamed the catalog file.

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 ramensoftware/windhawk@61d99ed8e1 (2026-09-12). Data as JSON: /api/errors/10ba05f8279419fd. Report an issue: GitHub.

Appendix: source

Thrown at src/windhawk-frontend/apps/windhawk-frontend/src/app/utils/swrHelpers.ts:19

/**
 * A response that arrived but reports a failure status. `fetch` resolves for any
 * status the server answers with and rejects only on a network error, so without
 * this the caller would be handed a 404's error page as the document it asked
 * for, and its own error branch would never run.
 */
export class HttpError extends Error {
  readonly status: number;

  constructor(status: number, statusText?: string) {
    super(`Request failed with status ${status}${statusText ? ` ${statusText}` : ''}`);
    this.name = 'HttpError';
    this.status = status;
  }
}

function assertOk(response: Response): Response {
  if (!response.ok) {
    throw new HttpError(response.status, response.statusText);
  }
  return response;
}

export const fetchText = (input: RequestInfo | URL, init?: RequestInit) =>
  fetch(input, init).then((res) => assertOk(res).text());

export const fetchJson = <T = unknown>(input: RequestInfo | URL, init?: RequestInit): Promise<T> =>
  fetch(input, init).then((res) => assertOk(res).json());

const CATALOG_BASE_URL = 'https://mods.windhawk.net/';

/**
 * A language tag, which is the only thing that names a catalog file. Anything
 * else is kept out of the URL, where a `/`, a `..` or a `?` would make it ask
 * for a different document than the one the path spells out.
 */
const LANGUAGE_TAG = /^[a-zA-Z]{2,8}(-[a-zA-Z0-9]{1,8})*$/;

View on GitHub (pinned to 61d99ed8e1)