{"record":{"id":"a2d32bc1a68dda6c","repo":"router-for-me/CLIProxyAPI","slug":"kimi-context-cancelled-w","errorCode":null,"errorMessage":"kimi: context cancelled: %w","messagePattern":"kimi: context cancelled: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/auth/kimi/kimi.go","lineNumber":247,"sourceCode":"\tif interval < defaultPollInterval {\n\t\tinterval = defaultPollInterval\n\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\tticker := time.NewTicker(interval)\n\tdefer ticker.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, fmt.Errorf(\"kimi: context cancelled: %w\", ctx.Err())\n\t\tcase <-ticker.C:\n\t\t\tif time.Now().After(deadline) {\n\t\t\t\treturn nil, fmt.Errorf(\"kimi: device code expired\")\n\t\t\t}\n\n\t\t\ttoken, pollErr, shouldContinue := c.exchangeDeviceCode(ctx, deviceCode.DeviceCode)\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\t// Continue polling\n\t\t}\n\t}\n}\n\n// exchangeDeviceCode attempts to exchange the device code for an access token.","sourceCodeStart":229,"sourceCodeEnd":265,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/auth/kimi/kimi.go#L229-L265","documentation":"The polling loop in PollForToken detected that its context was cancelled (or its deadline exceeded) while waiting for the user to complete authorization. The wrapped ctx.Err() is context.Canceled or context.DeadlineExceeded. No token was obtained; the device code itself may still be valid until it expires server-side.","triggerScenarios":"Caller of WaitForAuthorization/PollForToken cancels the ctx (user hits Ctrl-C during the 'visit this URL' wait), a parent context with a deadline shorter than the 15-minute maxPollDuration, server shutdown while a login is in progress.","commonSituations":"CLI login command with its own timeout shorter than user takes to authorize, TUI/manager cancelling in-flight logins on restart, context propagated from an HTTP request that times out.","solutions":["If the user cancelled intentionally, no fix needed — re-run login to start a fresh device flow","If DeadlineExceeded, give the context passed to PollForToken a deadline >= deviceCode.ExpiresIn (or the 15-minute maxPollDuration)","Ensure the ctx passed to WaitForAuthorization is not derived from a short-lived request scope; use a dedicated context for interactive login"],"exampleFix":"// before\nctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) // too short for user to authorize\nk.WaitForAuthorization(ctx, deviceCode)\n\n// after\nctx, cancel := context.WithCancel(context.Background())\ndefer cancel() // cancel only on user interrupt\nk.WaitForAuthorization(ctx, deviceCode)","handlingStrategy":"validation","validationCode":"// Give interactive login its own long-lived context before polling\nif _, ok := ctx.Deadline(); !ok {\n    var cancel context.CancelFunc\n    ctx, cancel = context.WithTimeout(ctx, 16*time.Minute)\n    defer cancel()\n}","typeGuard":null,"tryCatchPattern":"if err != nil && errors.Is(err, context.Canceled) {\n    // user aborted: clean up UI state, do not report as a bug\n}","preventionTips":["Never derive the login context from short-deadline request scopes","Cancel only on user intent (Ctrl-C), not timers","Distinguish Canceled vs DeadlineExceeded in handling"],"tags":["kimi","oauth","device-flow","context","cancellation"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}