{"record":{"id":"c854c06ac3623b56","repo":"sipeed/picoclaw","slug":"reading-device-token-response-w","errorCode":null,"errorMessage":"reading device token response: %w","messagePattern":"reading device token response: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/auth/oauth.go","lineNumber":422,"sourceCode":"\t})\n\n\tresp, err := http.Post(\n\t\tcfg.Issuer+\"/api/accounts/deviceauth/token\",\n\t\t\"application/json\",\n\t\tstrings.NewReader(string(reqBody)),\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"pending\")\n\t}\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"reading device token response: %w\", err)\n\t}\n\n\tvar tokenResp struct {\n\t\tAuthorizationCode string `json:\"authorization_code\"`\n\t\tCodeChallenge     string `json:\"code_challenge\"`\n\t\tCodeVerifier      string `json:\"code_verifier\"`\n\t}\n\tif err := json.Unmarshal(body, &tokenResp); err != nil {\n\t\treturn nil, err\n\t}\n\n\tredirectURI := cfg.Issuer + \"/deviceauth/callback\"\n\treturn ExchangeCodeForTokens(cfg, tokenResp.AuthorizationCode, tokenResp.CodeVerifier, redirectURI)\n}\n\nfunc RefreshAccessToken(cred *AuthCredential, cfg OAuthProviderConfig) (*AuthCredential, error) {\n\tif cred.RefreshToken == \"\" {\n\t\treturn nil, fmt.Errorf(\"no refresh token available\")","sourceCodeStart":404,"sourceCodeEnd":440,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/auth/oauth.go#L404-L440","documentation":"pollDeviceCode (pkg/auth/oauth.go:422) received HTTP 200 from {Issuer}/api/accounts/deviceauth/token (meaning the user has approved) but io.ReadAll failed while streaming the token payload. The authorization code inside that body is lost, so the poll must be retried.","triggerScenarios":"Connection dropped or context canceled after status 200 but before body completion on the deviceauth/token POST; proxy truncation; very short client timeout on a slow response.","commonSituations":"Flaky VPN/wifi mid-flow; corporate proxy idle-killing the stream; rare relative to pending/parse paths. Note LoginDeviceCode swallows this error and retries on the next tick; only callers of PollDeviceCodeOnce see it directly.","solutions":["Retry the poll: for device flows the server typically keeps the authorization available, the next poll re-POSTs and re-reads","Inspect the wrapped error for 'unexpected EOF' vs 'context deadline exceeded' to pick truncation vs timeout","Increase http.Client timeout if a custom short one is in use","Check intermediary proxies for response-size or idle limits"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":"func isTokenBodyReadError(err error) bool {\n\treturn err != nil && strings.Contains(err.Error(), \"reading device token response\")\n}","tryCatchPattern":"cred, err := auth.PollDeviceCodeOnce(cfg, deviceAuthID, userCode)\nif err != nil && isTokenBodyReadError(err) {\n\ttime.Sleep(interval)\n\tcred, err = auth.PollDeviceCodeOnce(cfg, deviceAuthID, userCode)\n}","preventionTips":["Treat body-read failures during polling as retriable 'pending', not fatal","Keep poll intervals at the server-advised value to avoid truncation under load","Use a client timeout comfortably larger than the largest expected token response","Surface poll errors in logs even when retrying"],"tags":["network","io","transient","device-code","oauth","go"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}