{"record":{"id":"512010fc5652b9a3","repo":"AlistGo/alist","slug":"lark-refresh-token-expired","errorCode":null,"errorMessage":"lark refresh token expired","messagePattern":"lark refresh token expired","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"drivers/lark/driver.go","lineNumber":196,"sourceCode":"\treturn []larkcore.RequestOptionFunc{larkcore.WithUserAccessToken(userAccessToken)}, nil\n}\n\nfunc (c *Lark) ensureUserAccessToken(ctx context.Context, forceRefresh bool) (string, error) {\n\tif strings.TrimSpace(c.RefreshToken) == \"\" {\n\t\treturn strings.TrimSpace(c.UserAccessToken), nil\n\t}\n\tif token := strings.TrimSpace(c.UserAccessToken); !forceRefresh && token != \"\" && !c.userAccessTokenExpired() {\n\t\treturn token, nil\n\t}\n\n\tc.tokenMu.Lock()\n\tdefer c.tokenMu.Unlock()\n\n\tif token := strings.TrimSpace(c.UserAccessToken); !forceRefresh && token != \"\" && !c.userAccessTokenExpired() {\n\t\treturn token, nil\n\t}\n\tif c.RefreshTokenExpiresAt > 0 && time.Now().After(time.Unix(c.RefreshTokenExpiresAt, 0)) {\n\t\treturn \"\", errors.New(\"lark refresh token expired\")\n\t}\n\n\tresp, err := c.client.Ext.Authen.RefreshAuthenAccessToken(ctx,\n\t\tlarkext.NewRefreshAuthenAccessTokenReqBuilder().\n\t\t\tBody(larkext.NewRefreshAuthenAccessTokenReqBodyBuilder().\n\t\t\t\tGrantType(larkext.GrantTypeRefreshCode).\n\t\t\t\tRefreshToken(strings.TrimSpace(c.RefreshToken)).\n\t\t\t\tBuild()).\n\t\t\tBuild())\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif !resp.Success() {\n\t\treturn \"\", errors.New(resp.Error())\n\t}\n\tif resp.Data == nil || resp.Data.AccessToken == \"\" {\n\t\treturn \"\", errors.New(\"lark refresh token response missing access token\")\n\t}","sourceCodeStart":178,"sourceCodeEnd":214,"githubUrl":"https://github.com/AlistGo/alist/blob/843d9dc8149126976b2625911e45a4d3ffd6f2f5/drivers/lark/driver.go#L178-L214","documentation":"The Lark (Feishu) driver refuses to refresh the user access token once time.Now() passes RefreshTokenExpiresAt. Lark refresh tokens are single-line-lifetime; after expiry only a fresh OAuth user-authorization code re-initiates the flow. The check runs under tokenMu after the fast-path cache lookup missed or expired.","triggerScenarios":"getUserAccessToken with an expired cached token AND c.RefreshTokenExpiresAt>0 AND now after that unix time. Happens on any Drive call after the storage has been idle longer than the refresh-token lifetime (typically tens of days) since last save.","commonSituations":"Storage unused for weeks/months then revisited; server clock skew on the alist host making local 'now' past expiry; RefreshExpiresIn previously saved short.","solutions":["Re-authorize: generate a new user access/refresh token pair via the Feishu OAuth flow and update the storage","Verify the alist host clock (NTP) — skew can fake expiry","Schedule a lightweight periodic Drive call (e.g. a List) to keep tokens refreshed before they lapse"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"if c.RefreshTokenExpiresAt > 0 && time.Now().After(time.Unix(c.RefreshTokenExpiresAt, 0)) {\n\treturn errors.New(\"lark refresh token already expired: re-authorize the storage\")\n}","typeGuard":"func (c *Lark) refreshTokenAlive() bool {\n\treturn c.RefreshTokenExpiresAt <= 0 || time.Now().Before(time.Unix(c.RefreshTokenExpiresAt, 0))\n}","tryCatchPattern":null,"preventionTips":["Keep storages warm with a scheduled lightweight call so tokens refresh before expiry","Monitor RefreshTokenExpiresAt and alert ahead of expiry; re-auth is manual once it lapses","Sync host clocks via NTP to avoid false expiry"],"tags":["lark","feishu","oauth","token-expiry","driver"],"backgroundTag":null,"analyzedSha":"843d9dc8149126976b2625911e45a4d3ffd6f2f5","analyzedAt":"2026-08-15T12:14:11.722Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}