{"record":{"id":"3f754298e671c43c","repo":"charmbracelet/crush","slug":"errordescription","errorCode":null,"errorMessage":"${ErrorDescription}","messagePattern":"\\$\\{ErrorDescription\\}","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/oauth/hyper/device.go","lineNumber":112,"sourceCode":"\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn \"\", ctx.Err()\n\t\tcase <-ticker.C:\n\t\t\tresult, err := pollOnce(ctx, deviceCode)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\tif result.RefreshToken != \"\" {\n\t\t\t\tevent.Alias(result.UserID)\n\t\t\t\treturn result.RefreshToken, nil\n\t\t\t}\n\t\t\tswitch result.Error {\n\t\t\tcase \"authorization_pending\":\n\t\t\t\tcontinue\n\t\t\tdefault:\n\t\t\t\treturn \"\", errors.New(result.ErrorDescription)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc pollOnce(ctx context.Context, deviceCode string) (TokenResponse, error) {\n\tvar result TokenResponse\n\turl := fmt.Sprintf(\"%s/device/auth/%s\", hyper.BaseURL(), deviceCode)\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn result, fmt.Errorf(\"create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"User-Agent\", \"crush\")\n\n\tclient := &http.Client{Timeout: 30 * time.Second}\n\tresp, err := client.Do(req)","sourceCodeStart":94,"sourceCodeEnd":130,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/oauth/hyper/device.go#L94-L130","documentation":"During the Hyper OAuth device-code flow, PollForToken receives an error field in the token response other than 'authorization_pending'. The server-supplied ErrorDescription is returned verbatim as a Go error, so the message content comes from the OAuth server.","triggerScenarios":"Polling the token endpoint while the device grant is in a terminal error state: the user denied the request, the device code expired (expired_token), or the server returned any unrecognized error code.","commonSituations":"User clicks 'cancel' on the GitHub-style authorization page and then the poller gets 'access_denied'; long waits past ExpiresIn yield 'expired_token'; network proxies inject unexpected error codes.","solutions":["Restart the device-code flow (call BeginDeviceAuth / request a fresh device code) since the current code is no longer pending.","Show the returned ErrorDescription to the user so they know what the authorization server rejected.","Handle well-known codes (slow_down, expired_token, access_denied) explicitly before treating the error as fatal.","Retry with a shorter polling interval if the error was slow_down."],"exampleFix":"// before\nres, err := flow.PollForToken(ctx, code)\nif err != nil { return err }\n// after\nres, err := flow.PollForToken(ctx, code)\nif err != nil {\n    if strings.Contains(err.Error(), \"expired_token\") {\n        return restartDeviceFlow(ctx)\n    }\n    return fmt.Errorf(\"device authorization failed: %w\", err)\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"token, err := flow.PollForToken(ctx, code)\nif err != nil {\n    switch {\n    case strings.Contains(err.Error(), \"slow_down\"):\n        // back off and retry\n    case strings.Contains(err.Error(), \"expired_token\"):\n        return restartDeviceFlow(ctx)\n    default:\n        return err\n    }\n}","preventionTips":["Respect the server's polling interval to avoid slow_down.","Complete authorization within the device code's ExpiresIn window.","Handle access_denied explicitly with a clear user message.","Wrap PollForToken with a bounded retry that restarts the flow on terminal errors."],"tags":["oauth","device-flow","hyper"],"backgroundTag":"oauth-device-flow-error","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}