router-for-me/CLIProxyAPI · error

kimi: empty access token in refresh response

Error message

kimi: empty access token in refresh response

What it means

Thrown by the Kimi OAuth device-flow client after a token refresh POST to kimiTokenURL returns HTTP 200 but the decoded JSON body has an empty or absent access_token field. The code explicitly guards tokenResp.AccessToken == "" after a successful json.Unmarshal (internal/auth/kimi/kimi.go:420). It means the upstream token endpoint responded 'successfully' yet did not issue a usable access token, so the refreshed KimiTokenData cannot be constructed.

Source

Thrown at internal/auth/kimi/kimi.go:421

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("kimi: refresh failed with status %d: %s", resp.StatusCode, string(bodyBytes))
	}

	var tokenResp struct {
		AccessToken  string  `json:"access_token"`
		RefreshToken string  `json:"refresh_token"`
		TokenType    string  `json:"token_type"`
		ExpiresIn    float64 `json:"expires_in"`
		Scope        string  `json:"scope"`
	}

	if err = json.Unmarshal(bodyBytes, &tokenResp); err != nil {
		return nil, fmt.Errorf("kimi: failed to parse refresh response: %w", err)
	}

	if tokenResp.AccessToken == "" {
		return nil, fmt.Errorf("kimi: empty access token in refresh response")
	}

	var expiresAt int64
	if tokenResp.ExpiresIn > 0 {
		expiresAt = time.Now().Unix() + int64(tokenResp.ExpiresIn)
	}

	return &KimiTokenData{
		AccessToken:  tokenResp.AccessToken,
		RefreshToken: tokenResp.RefreshToken,
		TokenType:    tokenResp.TokenType,
		ExpiresAt:    expiresAt,
		Scope:        tokenResp.Scope,
	}, nil
}

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Delete the stale Kimi credential file under auths/ and re-run the OAuth login flow to obtain a fresh refresh token
  2. Log the raw refresh response body when AccessToken is empty to see the server's actual error payload, then act on it
  3. Check whether the Kimi token endpoint URL or client_id (kimiClientID) changed in an upstream release and upgrade CLIProxyAPI
  4. If behind a corporate proxy, verify the token endpoint is reachable and returns genuine JSON (curl the token URL manually)

Example fix

// before (kimi.go:420)
if tokenResp.AccessToken == "" {
    return nil, fmt.Errorf("kimi: empty access token in refresh response")
}
// after: include the server payload for diagnosis (do not log tokens)
if tokenResp.AccessToken == "" {
    return nil, fmt.Errorf("kimi: empty access token in refresh response (status %d, body hint: %s)", resp.StatusCode, errorHintFromBody(bodyBytes))
}
Defensive patterns

Strategy: try-catch

Try / catch

if _, err := kimiClient.RefreshToken(ctx, refreshToken); err != nil {
    if strings.Contains(err.Error(), "empty access token in refresh response") {
        // treat as dead credential: purge and re-authenticate, do not retry
        _ = os.Remove(kimiAuthFile)
        return relogin()
    }
    return err
}

Prevention

When it happens

Trigger: Calling the Kimi refresh flow with grant_type=refresh_token where the refresh token is expired/revoked but the server still replies 200 with an error payload (e.g. {"error":"invalid_grant"}); an API change at the Kimi token endpoint renaming access_token; a proxy or CAPTCHA/HTML page returning 200 with an empty body that still unmarshals; fields returned as null.

Common situations: Kimi auth files in auths/ that are months old with a stale refresh token; Moonshot/Kimi changing their OAuth endpoint contract; running the proxy behind a captive portal or corporate proxy that injects a 200 HTML response; clock/env issues causing the server to soft-fail.

Related errors


AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15). Data as JSON: /api/errors/738ffbc4af61222a. Report an issue: GitHub.