AlistGo/alist · critical

failed to refresh token: refresh token is empty

Error message

failed to refresh token: refresh token is empty

What it means

Thrown by the legacy AliDrive (aliyundrive personal) token refresh when the open.aliyundrive.com OAuth token endpoint returns HTTP success with no error code but an EMPTY refresh_token in the response body. Because the driver rotates refresh tokens (each may be single-use), an empty new refresh token would brick the storage, so it aborts before overwriting stored credentials.

Source

Thrown at drivers/aliyundrive/util.go:77

func (d *AliDrive) refreshToken() error {
	url := "https://auth.alipan.com/v2/account/token"
	var resp base.TokenResp
	var e RespErr
	_, err := base.RestyClient.R().
		//ForceContentType("application/json").
		SetBody(base.Json{"refresh_token": d.RefreshToken, "grant_type": "refresh_token"}).
		SetResult(&resp).
		SetError(&e).
		Post(url)
	if err != nil {
		return err
	}
	if e.Code != "" {
		return fmt.Errorf("failed to refresh token: %s", e.Message)
	}
	if resp.RefreshToken == "" {
		return errors.New("failed to refresh token: refresh token is empty")
	}
	d.RefreshToken, d.AccessToken = resp.RefreshToken, resp.AccessToken
	op.MustSaveDriverStorage(d)
	return nil
}

func (d *AliDrive) request(url, method string, callback base.ReqCallback, resp interface{}) ([]byte, error, RespErr) {
	req := base.RestyClient.R()
	state, ok := global.Load(d.UserID)
	if !ok {
		if url == "https://api.alipan.com/v2/user/get" {
			state = &State{}
		} else {
			return nil, fmt.Errorf("can't load user state, user_id: %s", d.UserID), RespErr{}
		}
	}
	req.SetHeaders(map[string]string{
		"Authorization": "Bearer\t" + d.AccessToken,

View on GitHub (pinned to 843d9dc814)

Solutions

  1. Ensure only ONE deployment/refresh-token holder uses this refresh token — concurrent refreshers consume each other's rotated tokens; disable the duplicate instance
  2. Re-obtain a fresh refresh_token (re-run the QR/token acquisition for the driver) and update the storage config
  3. Check e.Code/response body by enabling request logging to confirm whether the endpoint actually returned an error payload the code missed
  4. If the account was migrated to the Open Platform, switch to the aliyundrive_open driver instead of the legacy one

Example fix

// before
if resp.RefreshToken == "" {
    return errors.New("failed to refresh token: refresh token is empty")
}

// after — surface the raw payload for diagnosis
if resp.RefreshToken == "" {
    return fmt.Errorf("failed to refresh token: refresh token is empty, resp: %s", res.String())
}
Defensive patterns

Strategy: validation

Validate before calling

// Before mounting, sanity-check the configured refresh token
if d.RefreshToken == "" {
    return errors.New("refresh_token missing; re-authorize the storage")
}
// ensure no other instance refreshes with the same token
// (single owner per refresh token)

Type guard

null

Try / catch

if err := d.refreshToken(); err != nil {
    if strings.Contains(err.Error(), "refresh token is empty") {
        // credentials are unusable: stop retrying and prompt re-authorization
    }
    return err
}

Prevention

When it happens

Trigger: POST to the token endpoint with grant_type=refresh_token succeeds (no e.Code), but resp.RefreshToken == "" — server quirk, truncated body, rate limiting, or an account-side issue returning a partial payload.

Common situations: Long-running aliyundrive mounts whose stored refresh token was already consumed by another instance (two Alist deployments sharing one token); API endpoint behavior changes after alipan.com migration; network middleboxes mangling response bodies; token revoked from the Aliyun console.

Related errors


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