{"record":{"id":"d2a6a049e637ca0a","repo":"toeverything/AFFiNE","slug":"unsupported-client-version","errorCode":"unsupported_client_version","errorMessage":"Unsupported client with version [${clientVersion}], required version is [${requiredVersion}].","messagePattern":"Unsupported client with version \\[(.+?)\\], required version is \\[(.+?)\\]\\.","errorType":"exception","errorClass":"UnsupportedClientVersion","httpStatus":403,"severity":"error","filePath":"packages/backend/server/src/core/auth/guard.ts","lineNumber":256,"sourceCode":"      .authSessionId;\n    if (authSessionId) {\n      await this.authSessions.revoke(\n        authSessionId,\n        'unsupported_client_version',\n        session.user.id\n      );\n    } else {\n      await this.auth.signOut(session.sessionId);\n    }\n    if (res && !authSessionId) {\n      await this.auth.refreshCookies(res, session.sessionId);\n    }\n\n    if (isPublic && !authSessionId) {\n      return false;\n    }\n\n    throw new UnsupportedClientVersion({\n      clientVersion: clientVersion ?? 'unset_or_invalid',\n      requiredVersion: versionCheckResult.requiredVersion,\n    });\n  }\n\n  private getVersionRange(versionRange: string): semver.Range | null {\n    if (this.cachedVersionRange.has(versionRange)) {\n      // oxlint-disable-next-line typescript/no-non-null-assertion\n      return this.cachedVersionRange.get(versionRange)!;\n    }\n\n    let range: semver.Range | null = null;\n    try {\n      range = new semver.Range(versionRange, { loose: false });\n      if (!semver.validRange(range)) {\n        range = null;\n      }\n    } catch {","sourceCodeStart":238,"sourceCodeEnd":274,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/auth/guard.ts#L238-L274","documentation":"The client app version fails the server's semver gate. In checkUserSessionClientVersion the guard reads the version from the request header, falls back to session.refreshClientVersion / signInClientVersion, then runs it through checkClientVersion against config.client.versionControl.requiredVersion and a hard-coded HARD_REQUIRED_VERSION floor. If it fails, the session is revoked (native auth session) or signed out (cookie session) first, and — unless the route is public with a cookie session — UnsupportedClientVersion is thrown with the offending clientVersion and the requiredVersion. Category is action_forbidden; the data payload exposes both versions for the UI.","triggerScenarios":"A request whose X-Client-Version header (or stored session signInClientVersion) does not satisfy the configured requiredVersion semver range, while config.client.versionControl.enabled is true. For canary builds the canary date must be current or it falls back to the canary required version. The route is either non-public, or public but using a native auth session (authSessionId present), so the early 'return false' escape is not taken.","commonSituations":"User is running an old desktop/mobile build against a server that has bumped requiredVersion (e.g. >=0.26.0). A self-hosted admin set client.versionControl.requiredVersion too strictly or left it mismatched across nodes. The client forgot to send the version header, so clientVersion resolves to 'unset_or_invalid'.","solutions":["Update the client app to a release that satisfies config.client.versionControl.requiredVersion.","If appropriate, relax the server config (AFFiNE_CLIENT_VERSION_CONTROL_REQUIRED_VERSION / config file) to include the installed client range, then redeploy.","Ensure the client sends the correct version header on every request so clientVersion is not 'unset_or_invalid'.","Temporarily disable the gate by setting client.versionControl.enabled=false while upgrading the fleet."],"exampleFix":"# self-hosted: broaden the allowed range\nAFFiNE_CLIENT_VERSION_CONTROL_ENABLED=true\nAFFiNE_CLIENT_VERSION_CONTROL_REQUIRED_VERSION=\">=0.25.0\"","handlingStrategy":"validation","validationCode":"import semver from 'semver';\n\nfunction isClientVersionAllowed(\n  clientVersion: string | undefined,\n  requiredRange: string\n): boolean {\n  if (!clientVersion) return false;\n  try {\n    return semver.satisfies(clientVersion, requiredRange, {\n      includePrerelease: true,\n    });\n  } catch {\n    return false;\n  }\n}\n\n// before any authenticated call:\nif (!isClientVersionAllowed(APP_VERSION, SERVER_REQUIRED_VERSION)) {\n  promptUpgrade();\n}","typeGuard":"function isUnsupportedClientVersion(err: unknown): boolean {\n  return (\n    !!err &&\n    typeof err === 'object' &&\n    (err as { code?: string }).code === 'unsupported_client_version'\n  );\n}","tryCatchPattern":"try {\n  await api.call();\n} catch (err) {\n  if (isUnsupportedClientVersion(err)) {\n    showUpgradeRequired((err as any).data.requiredVersion);\n    return;\n  }\n  throw err;\n}","preventionTips":["Send a stable X-Client-Version header on every request from the client.","Run a pre-flight version check against the server's /version endpoint before issuing authenticated requests.","Keep requiredVersion config aligned across all server nodes in a cluster.","Test client builds against the configured requiredVersion range in CI."],"tags":["auth","versioning","semver","config","client-compat"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}