{"record":{"id":"738ffbc4af61222a","repo":"router-for-me/CLIProxyAPI","slug":"kimi-empty-access-token-in-refresh-response","errorCode":null,"errorMessage":"kimi: empty access token in refresh response","messagePattern":"kimi: empty access token in refresh response","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/auth/kimi/kimi.go","lineNumber":421,"sourceCode":"\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"kimi: refresh failed with status %d: %s\", resp.StatusCode, string(bodyBytes))\n\t}\n\n\tvar tokenResp struct {\n\t\tAccessToken  string  `json:\"access_token\"`\n\t\tRefreshToken string  `json:\"refresh_token\"`\n\t\tTokenType    string  `json:\"token_type\"`\n\t\tExpiresIn    float64 `json:\"expires_in\"`\n\t\tScope        string  `json:\"scope\"`\n\t}\n\n\tif err = json.Unmarshal(bodyBytes, &tokenResp); err != nil {\n\t\treturn nil, fmt.Errorf(\"kimi: failed to parse refresh response: %w\", err)\n\t}\n\n\tif tokenResp.AccessToken == \"\" {\n\t\treturn nil, fmt.Errorf(\"kimi: empty access token in refresh response\")\n\t}\n\n\tvar expiresAt int64\n\tif tokenResp.ExpiresIn > 0 {\n\t\texpiresAt = time.Now().Unix() + int64(tokenResp.ExpiresIn)\n\t}\n\n\treturn &KimiTokenData{\n\t\tAccessToken:  tokenResp.AccessToken,\n\t\tRefreshToken: tokenResp.RefreshToken,\n\t\tTokenType:    tokenResp.TokenType,\n\t\tExpiresAt:    expiresAt,\n\t\tScope:        tokenResp.Scope,\n\t}, nil\n}\n","sourceCodeStart":403,"sourceCodeEnd":437,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/auth/kimi/kimi.go#L403-L437","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Delete the stale Kimi credential file under auths/ and re-run the OAuth login flow to obtain a fresh refresh token","Log the raw refresh response body when AccessToken is empty to see the server's actual error payload, then act on it","Check whether the Kimi token endpoint URL or client_id (kimiClientID) changed in an upstream release and upgrade CLIProxyAPI","If behind a corporate proxy, verify the token endpoint is reachable and returns genuine JSON (curl the token URL manually)"],"exampleFix":"// before (kimi.go:420)\nif tokenResp.AccessToken == \"\" {\n    return nil, fmt.Errorf(\"kimi: empty access token in refresh response\")\n}\n// after: include the server payload for diagnosis (do not log tokens)\nif tokenResp.AccessToken == \"\" {\n    return nil, fmt.Errorf(\"kimi: empty access token in refresh response (status %d, body hint: %s)\", resp.StatusCode, errorHintFromBody(bodyBytes))\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if _, err := kimiClient.RefreshToken(ctx, refreshToken); err != nil {\n    if strings.Contains(err.Error(), \"empty access token in refresh response\") {\n        // treat as dead credential: purge and re-authenticate, do not retry\n        _ = os.Remove(kimiAuthFile)\n        return relogin()\n    }\n    return err\n}","preventionTips":["Keep Kimi credentials fresh by exercising the token before long expiry windows","Wrap the refresh call and log (safe, non-secret) response metadata when it fails","Automate re-login when refresh repeatedly fails instead of retrying the same stale refresh token"],"tags":["oauth","kimi","auth","token-refresh"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}