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
- Log error.status to identify the exact HTTP failure and fix the URL/request accordingly
- Verify the endpoint is reachable (curl the URL) and the server is healthy
- 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
- Check response.status before consuming parsed data
- Use HEAD/health checks before dependent fetches
- Centralize fetch handling through assertOk so all errors are HttpError instances
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)