{"record":{"id":"dc930fdc873861e7","repo":"router-for-me/CLIProxyAPI","slug":"xai-device-code-context-cancelled-w","errorCode":null,"errorMessage":"xai device code: context cancelled: %w","messagePattern":"xai device code: context cancelled: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/auth/xai/xai.go","lineNumber":235,"sourceCode":"\t}\n\n\tdeadline := time.Now().Add(MaxPollDuration)\n\tif deviceCode.ExpiresIn > 0 {\n\t\tcodeDeadline := time.Now().Add(time.Duration(deviceCode.ExpiresIn) * time.Second)\n\t\tif codeDeadline.Before(deadline) {\n\t\t\tdeadline = codeDeadline\n\t\t}\n\t}\n\n\t// Poll immediately once, then wait between subsequent attempts.\n\tfirstAttempt := true\n\ttimer := time.NewTimer(0)\n\tdefer timer.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, fmt.Errorf(\"xai device code: context cancelled: %w\", ctx.Err())\n\t\tcase <-timer.C:\n\t\t\tif !firstAttempt && time.Now().After(deadline) {\n\t\t\t\treturn nil, fmt.Errorf(\"xai device code expired\")\n\t\t\t}\n\t\t\tfirstAttempt = false\n\n\t\t\ttoken, pollErr, nextInterval, shouldContinue := a.exchangeDeviceCode(ctx, tokenEndpoint, deviceCode.DeviceCode, interval)\n\t\t\tif token != nil {\n\t\t\t\treturn token, nil\n\t\t\t}\n\t\t\tif !shouldContinue {\n\t\t\t\treturn nil, pollErr\n\t\t\t}\n\t\t\tinterval = nextInterval\n\t\t\ttimer.Reset(interval)\n\t\t}\n\t}\n}","sourceCodeStart":217,"sourceCodeEnd":253,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/auth/xai/xai.go#L217-L253","documentation":"The context passed to PollForToken was cancelled or its deadline expired while the poll loop was waiting for user authorization. The select on ctx.Done() aborts the device flow and wraps context.Canceled or context.DeadlineExceeded.","triggerScenarios":"Any cancellation of the ctx given to PollForToken/WaitForAuthorization during the polling window: user Ctrl-C, parent request timeout, shutdown of the calling service.","commonSituations":"A short HTTP request context reused for an interactive device flow that can legitimately take minutes; orchestrator (e.g. CLI command) timing out; graceful shutdown cancelling in-flight auth.","solutions":["Give the device flow its own context with a timeout >= the device code lifetime (typically expires_in from the authorization response)","Derive from context.Background() for interactive flows instead of a request-scoped context","If cancellation was intentional (shutdown), treat this as a clean abort rather than an error"],"exampleFix":"// before\ntokenData, err := auth.WaitForAuthorization(reqCtx, deviceCode) // reqCtx has 30s deadline\n\n// after\nauthCtx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)\ndefer cancel()\ntokenData, err := auth.WaitForAuthorization(authCtx, deviceCode)","handlingStrategy":"validation","validationCode":"authCtx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) // >= device code lifetime\ndefer cancel()\ntokenData, err := auth.WaitForAuthorization(authCtx, deviceCode)","typeGuard":null,"tryCatchPattern":"tokenData, err := auth.WaitForAuthorization(authCtx, deviceCode)\nif err != nil {\n    if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {\n        // intentional abort or timeout: clean exit, optionally restart flow\n        return err\n    }\n    return err\n}","preventionTips":["Never reuse a short request-scoped context for an interactive device flow","Size the context timeout to the device code expires_in plus margin","Treat cancellation during shutdown as a clean abort, not a failure"],"tags":["xai","auth","context","cancellation"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}