Mintplex-Labs/anything-llm · warning

Forbidden

Error message

Forbidden

What it means

This 403 'Forbidden' is a deliberate status, not an exception, sent by GET /system/check-token (server/endpoints/system.js:134) in multi-user mode. After validatedRequest passes, the handler loads the session user via userFromSession(); it sends 403 when the lookup returns null (no Authorization header, JWT invalid/expired because JWT_SECRET changed, or the user row was deleted) or when user.suspended is true (admin suspended the account). Single-user mode never returns 403 here - it always answers 200.

Source

Thrown at server/endpoints/system.js:136

  app.get("/setup-complete", async (_, response) => {
    try {
      const results = await SystemSettings.currentSettings();
      response.status(200).json({ results });
    } catch (e) {
      console.error(e.message, e);
      response.sendStatus(500).end();
    }
  });

  app.get(
    "/system/check-token",
    [validatedRequest],
    async (request, response) => {
      try {
        if (multiUserMode(response)) {
          const user = await userFromSession(request, response);
          if (!user || user.suspended) {
            response.sendStatus(403).end();
            return;
          }

          response.sendStatus(200).end();
          return;
        }

        response.sendStatus(200).end();
      } catch (e) {
        console.error(e.message, e);
        response.sendStatus(500).end();
      }
    }
  );

  /**
   * Refreshes the user object from the session from a provided token.
   * This does not refresh the token itself - if that is expired or invalid, the user will be logged out.

View on GitHub (pinned to 20f6d3546c)

Solutions

  1. Treat 403 from check-token as 'log the user out': clear the stored session token and redirect to /login
  2. If all users are logged out at once, check whether JWT_SECRET was rotated (update-password regenerates it) - users simply sign in again
  3. If a single user is affected, verify their account is not suspended in the admin users panel
  4. Ensure the client sends Authorization: Bearer <token> on the request

Example fix

// before (frontend)
const res = await fetch("/api/system/check-token");
if (!res.ok) throw new Error("unexpected");

// after
const res = await fetch("/api/system/check-token", {
  headers: { Authorization: `Bearer ${window.storage.getItem("token")}` },
});
if (res.status === 403) {
  window.storage.removeItem("token");
  window.location = "/login"; // session invalid or user suspended
}
Defensive patterns

Strategy: validation

Validate before calling

const token = window.localStorage.getItem("token");
if (!token) {
  window.location = "/login"; // no session to check - skip the request entirely
}
const res = await fetch("/api/system/check-token", {
  headers: { Authorization: `Bearer ${token}` },
});
if (res.status === 403) {
  window.localStorage.removeItem("token");
  window.location = "/login";
}

Prevention

When it happens

Trigger: Frontend polling /system/check-token with a stale session token after the admin rotated JWT_SECRET (e.g. via /system/update-password which regenerates the secret with v4()); a suspended user whose browser still holds a valid-signed JWT; a deleted user with an unexpired token; missing Authorization: Bearer header.

Common situations: Password update in single-user-to-multi-user transitions invalidating old sessions; admin suspending a user mid-session; clock skew or JWT_EXPIRY misconfiguration making tokens expire immediately; frontend kept a token from a previous deployment whose JWT_SECRET was regenerated.

Understand the failure class

Related errors


AI-assisted analysis of Mintplex-Labs/anything-llm@20f6d3546c (2026-08-18). Data as JSON: /api/errors/bc9d1776e4806223. Report an issue: GitHub.