AlistGo/alist · critical

lark refresh token expired

Error message

lark refresh token expired

What it means

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.

Source

Thrown at drivers/lark/driver.go:196

	return []larkcore.RequestOptionFunc{larkcore.WithUserAccessToken(userAccessToken)}, nil
}

func (c *Lark) ensureUserAccessToken(ctx context.Context, forceRefresh bool) (string, error) {
	if strings.TrimSpace(c.RefreshToken) == "" {
		return strings.TrimSpace(c.UserAccessToken), nil
	}
	if token := strings.TrimSpace(c.UserAccessToken); !forceRefresh && token != "" && !c.userAccessTokenExpired() {
		return token, nil
	}

	c.tokenMu.Lock()
	defer c.tokenMu.Unlock()

	if token := strings.TrimSpace(c.UserAccessToken); !forceRefresh && token != "" && !c.userAccessTokenExpired() {
		return token, nil
	}
	if c.RefreshTokenExpiresAt > 0 && time.Now().After(time.Unix(c.RefreshTokenExpiresAt, 0)) {
		return "", errors.New("lark refresh token expired")
	}

	resp, err := c.client.Ext.Authen.RefreshAuthenAccessToken(ctx,
		larkext.NewRefreshAuthenAccessTokenReqBuilder().
			Body(larkext.NewRefreshAuthenAccessTokenReqBodyBuilder().
				GrantType(larkext.GrantTypeRefreshCode).
				RefreshToken(strings.TrimSpace(c.RefreshToken)).
				Build()).
			Build())
	if err != nil {
		return "", err
	}
	if !resp.Success() {
		return "", errors.New(resp.Error())
	}
	if resp.Data == nil || resp.Data.AccessToken == "" {
		return "", errors.New("lark refresh token response missing access token")
	}

View on GitHub (pinned to 843d9dc814)

Solutions

  1. Re-authorize: generate a new user access/refresh token pair via the Feishu OAuth flow and update the storage
  2. Verify the alist host clock (NTP) — skew can fake expiry
  3. Schedule a lightweight periodic Drive call (e.g. a List) to keep tokens refreshed before they lapse
Defensive patterns

Strategy: validation

Validate before calling

if c.RefreshTokenExpiresAt > 0 && time.Now().After(time.Unix(c.RefreshTokenExpiresAt, 0)) {
	return errors.New("lark refresh token already expired: re-authorize the storage")
}

Type guard

func (c *Lark) refreshTokenAlive() bool {
	return c.RefreshTokenExpiresAt <= 0 || time.Now().Before(time.Unix(c.RefreshTokenExpiresAt, 0))
}

Prevention

When it happens

Trigger: 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.

Common situations: Storage unused for weeks/months then revisited; server clock skew on the alist host making local 'now' past expiry; RefreshExpiresIn previously saved short.

Understand the failure class

Related errors


AI-assisted analysis of AlistGo/alist@843d9dc814 (2026-08-15). Data as JSON: /api/errors/512010fc5652b9a3. Report an issue: GitHub.