MHSanaei/3x-ui · error · HttpError

Request failed with status ${status}

Error message

Request failed with status ${status}

What it means

HttpError is thrown by the shared httpRequest wrapper in frontend/src/api/http-init.ts after any response where res.ok is false (status outside 200-299) that was not already handled as 401 (session-expiry redirect) or retried as 403 (CSRF refresh). The message template 'Request failed with status ${status}' is the generic HttpError message; the thrown value carries status, statusText, and the parsed body so callers can branch on it. It is the single choke point for all panel REST failures, so server-side error messages arrive as err.data (often {msg: ...}).

Source

Thrown at frontend/src/api/http-init.ts:193

  if (res.status === 403 && !SAFE_METHODS.has(method.toUpperCase())) {
    csrfToken = null;
    const fresh = await fetchCsrfToken();
    if (fresh) {
      csrfToken = fresh;
      res = await performFetch(method, url, data, options, fresh);
    }
  }

  if (res.status === 401) {
    if (!sessionExpired) {
      sessionExpired = true;
      window.location.replace(window.X_UI_BASE_PATH || basePathPrefix || '/');
    }
    return new Promise<HttpResponse>(() => {});
  }

  const parsed = await parseBody(res);
  if (!res.ok) throw new HttpError(res.status, res.statusText, parsed);
  return { ok: true, status: res.status, statusText: res.statusText, data: parsed };
}

export function setupHttp(): void {
  let basePath: string | null | undefined = window.X_UI_BASE_PATH;
  if (!basePath) {
    const metaTag = document.querySelector('meta[name="base-path"]');
    basePath = metaTag ? metaTag.getAttribute('content') : null;
  }
  basePathPrefix =
    typeof basePath === 'string' && basePath !== '' && basePath !== '/'
      ? basePath.replace(/\/$/, '')
      : '';

  csrfToken = readMetaToken();
}

View on GitHub (pinned to ad32144c42)

Solutions

  1. Inspect the thrown HttpError: read err.status and err.data.msg — the Go side almost always puts the human-readable reason in data.msg
  2. If status is 403 repeatedly, confirm the CSRF meta tag / X-CSRF-Token flow is intact (setupHttp ran before requests)
  3. If 401s leak through on safe methods, verify window.X_UI_BASE_PATH matches the serving base path so the redirect lands on the login page
  4. For 5xx, check the Go panel logs for the matching request error

Example fix

// before
const res = await httpRequest('POST', '/panel/api/inbounds/add', payload);

// after
try {
  const res = await httpRequest('POST', '/panel/api/inbounds/add', payload);
} catch (e) {
  if (e instanceof HttpError) {
    message.error(String((e.data as { msg?: string })?.msg ?? e.statusText));
    return;
  }
  throw e;
}
Defensive patterns

Strategy: try-catch

Type guard

export function isHttpError(e: unknown): e is HttpError {
  return e instanceof HttpError && typeof e.status === 'number';
}

Try / catch

try {
  await httpRequest('POST', url, payload);
} catch (e) {
  if (isHttpError(e)) {
    if (e.status === 401) return; // redirect already handled
    message.error(String((e.data as { msg?: string })?.msg ?? e.statusText));
    return;
  }
  throw e; // network failure etc.
}

Prevention

When it happens

Trigger: Any POST/GET to /panel/api/* returning 4xx/5xx: submitting an inbound with invalid data (400 with JSON body), hitting a route behind session auth after cookie loss but before the 401 handler fires on a safe method, 500s from panicking handlers, or gateway timeouts. Safe-method CSRF 403s are retried once; a second 403 falls through to this throw.

Common situations: Form validation errors surfaced from Gin ShouldBind; expired sessions on non-redirected requests; backend route changed but frontend stale (dev server not restarted after rebuild); reverse proxy returning 502 while the panel restarts.

Related errors


AI-assisted analysis of MHSanaei/3x-ui@ad32144c42 (2026-08-15). Data as JSON: /api/errors/49f342ee1b270338. Report an issue: GitHub.