Mintplex-Labs/anything-llm · error

Invalid or expired registration token

Error message

Invalid or expired registration token

What it means

validRegistrationToken calls MobileDevice.tempToken(token), which checks an in-memory Map (TemporaryMobileDeviceRequests). It returns null — yielding 400 { error: 'Invalid or expired registration token' } — when the token is unknown, expired (tokens live 3 minutes: expiresAt = createdAt + 3*60_000), or already consumed: a finally block deletes the entry on every lookup, so each temp token works exactly once. The Map also dies with the process on restart.

Source

Thrown at server/endpoints/mobile/middleware/index.js:64

 * and associates the user with the token (if valid). Temporary token is consumed
 * and cannot be used again after this middleware is called.
 * @param {*} request
 * @param {*} response
 * @param {*} next
 */
async function validRegistrationToken(request, response, next) {
  try {
    const authHeader = request.header("Authorization");
    const tempToken = authHeader ? authHeader.split(" ")[1] : null;
    if (!tempToken)
      return response
        .status(400)
        .json({ error: "Registration token is required" });

    const tempTokenData = MobileDevice.tempToken(tempToken);
    if (!tempTokenData)
      return response
        .status(400)
        .json({ error: "Invalid or expired registration token" });

    // If in multi-user mode, we need to validate the user id
    // associated exists, is not banned and then associate with locals so we can reuse it later.
    // If not in multi-user mode then simply having a valid token is enough.
    const multiUserMode = await SystemSettings.isMultiUserMode();
    if (multiUserMode) {
      if (!tempTokenData.userId)
        return response
          .status(400)
          .json({ error: "User id not found in registration token" });
      const user = await User.get({ id: Number(tempTokenData.userId) });
      if (!user) return response.status(400).json({ error: "User not found" });
      if (user.suspended)
        return response
          .status(400)
          .json({ error: "User is suspended - cannot register device" });
      response.locals.user = user;

View on GitHub (pinned to 3aec848f28)

Solutions

  1. Fetch fresh connect-info (GET /api/mobile/connect-info) or rescan the QR and register immediately, within 3 minutes
  2. Never retry register with the same temp token — always obtain a new one first
  3. Pin connect-info and register to the same server instance; temp tokens are in-process memory, not shared state

Example fix

// before — reusing a stale token
await register(tempToken); // 400 invalid or expired
await register(tempToken); // retry, still fails

// after — refresh token, then register once
const info = await (await fetch('/api/mobile/connect-info')).json();
const fresh = new URL(info.connectionUrl).searchParams.get('t');
await register(fresh);
Defensive patterns

Strategy: retry

Validate before calling

const TOKEN_TTL_MS = 3 * 60_000;
if (Date.now() - tokenFetchedAt > TOKEN_TTL_MS - 5_000) {
  tempToken = await fetchFreshConnectInfo(); // avoid expired-token 400
}

Try / catch

try {
  await register(tempToken);
} catch (e) {
  if (e.status === 400 && e.body?.error?.includes('Invalid or expired registration token')) {
    const fresh = await fetchFreshConnectInfo(); // one retry with a new token
    return register(fresh);
  }
  throw e;
}

Prevention

When it happens

Trigger: Scanning the QR or copying the connect-info URL and registering more than 3 minutes later; retrying /mobile/register after a prior attempt (the first call consumed the token); server restart or hot-reload between connect-info and register; token typo; load balancer routing register to a different instance than the one holding the Map.

Common situations: Manual typing of the pairing URL in dev; nodemon/PM2 restart wiping in-memory state; user rescans a new QR but the app caches the old URL; horizontally scaled deployments.

Related errors


AI-assisted analysis of Mintplex-Labs/anything-llm@3aec848f28 (2026-08-18). Data as JSON: /api/errors/9443b027f4449fa4. Report an issue: GitHub.