{"record":{"id":"dff64d8f2c1d233c","repo":"Devolutions/UniGetUI","slug":"the-pending-github-device-flow-has-expired-start","errorCode":null,"errorMessage":"The pending GitHub device flow has expired. Start sign-in again.","messagePattern":"The pending GitHub device flow has expired\\. Start sign-in again\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"warning","filePath":"src/UniGetUI.Interface.IpcApi/IpcBackupApi.cs","lineNumber":211,"sourceCode":"        {\n            Status = \"success\",\n            Command = \"start-github-sign-in\",\n            Message = request.LaunchBrowser\n                ? \"GitHub device flow started and the verification page was opened.\"\n                : \"GitHub device flow started.\",\n            Auth = await GetGitHubAuthInfoAsync(),\n        };\n    }\n\n    public static async Task<IpcGitHubAuthResult> CompleteGitHubDeviceFlowAsync()\n    {\n        EnsureGitHubClientConfigured();\n\n        PendingGitHubDeviceFlow pending = GetPendingGitHubDeviceFlow();\n        if (DateTimeOffset.UtcNow >= pending.ExpiresAtUtc)\n        {\n            ClearPendingGitHubDeviceFlow();\n            throw new InvalidOperationException(\n                \"The pending GitHub device flow has expired. Start sign-in again.\"\n            );\n        }\n\n        try\n        {\n            using var client = CreateAnonymousGitHubClient();\n            var token = await client.CreateAccessTokenForDeviceFlowAsync(\n                Secrets.GetGitHubClientId(),\n                pending.DeviceFlow,\n                CancellationToken.None\n            );\n\n            if (string.IsNullOrWhiteSpace(token.AccessToken))\n            {\n                throw new InvalidOperationException(\"GitHub did not return an access token.\");\n            }\n","sourceCodeStart":193,"sourceCodeEnd":229,"githubUrl":"https://github.com/Devolutions/UniGetUI/blob/9b1d7d0eab91620fc15c86b36676532095936ae3/src/UniGetUI.Interface.IpcApi/IpcBackupApi.cs#L193-L229","documentation":"CompleteGitHubDeviceFlowAsync retrieves the pending device flow (GetPendingGitHubDeviceFlow throws if none). It then compares DateTimeOffset.UtcNow against pending.ExpiresAtUtc, which was computed as UtcNow.AddSeconds(deviceFlow.ExpiresIn) at initiation. If the current time has passed the expiry, it clears the pending flow and throws InvalidOperationException. GitHub device codes typically expire after 15 minutes.","triggerScenarios":"StartGitHubDeviceFlowAsync was called to begin the flow, but the user took longer than the device-code lifetime (ExpiresIn seconds) to enter the code in the browser. By the time CompleteGitHubDeviceFlowAsync polls, DateTimeOffset.UtcNow >= pending.ExpiresAtUtc.","commonSituations":"The user walked away after the device-flow UI was shown and returned after 15+ minutes. The polling client had a long delay or retry loop that overshot the expiry. The system clock drifted significantly. The flow was started but never completed in the same session.","solutions":["Call StartGitHubDeviceFlowAsync again to obtain a fresh device code, then complete it within the new ExpiresIn window.","Poll CompleteGitHubDeviceFlowAsync at the interval returned by the device flow (PollIntervalSeconds) to avoid missing the window.","Check the ExpiresAt timestamp from IpcGitHubAuthInfo and surface a countdown to the user so they act in time."],"exampleFix":"// before: completing after the code expired\nawait IpcBackupApi.CompleteGitHubDeviceFlowAsync();\n// after: restart the flow when expired\nvar auth = await IpcBackupApi.GetStatusAsync();\nif (auth.Auth.DeviceFlowPending && auth.Auth.ExpiresAt > DateTimeOffset.UtcNow)\n    await IpcBackupApi.CompleteGitHubDeviceFlowAsync();\nelse\n    await IpcBackupApi.StartGitHubDeviceFlowAsync(new IpcGitHubDeviceFlowRequest { LaunchBrowser = true });","handlingStrategy":"validation","validationCode":"var auth = await IpcBackupApi.GetStatusAsync();\nif (auth.Auth.DeviceFlowPending && auth.Auth.ExpiresAt > DateTimeOffset.UtcNow)\n    await IpcBackupApi.CompleteGitHubDeviceFlowAsync();\nelse\n    await IpcBackupApi.StartGitHubDeviceFlowAsync(new IpcGitHubDeviceFlowRequest { LaunchBrowser = true });","typeGuard":"static bool DeviceFlowStillValid(IpcGitHubAuthInfo auth) =>\n    auth.DeviceFlowPending && auth.ExpiresAt is { } exp && DateTimeOffset.UtcNow < exp;","tryCatchPattern":"try { await IpcBackupApi.CompleteGitHubDeviceFlowAsync(); }\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"expired\"))\n{ await IpcBackupApi.StartGitHubDeviceFlowAsync(new IpcGitHubDeviceFlowRequest { LaunchBrowser = true }); }","preventionTips":["Poll at PollIntervalSeconds to complete before the device code expires.","Show the user a countdown based on ExpiresAt.","Restart the flow immediately if expired rather than retrying the stale code."],"tags":["github","oauth","device-flow","authentication","ipc"],"backgroundTag":null,"analyzedSha":"9b1d7d0eab91620fc15c86b36676532095936ae3","analyzedAt":"2026-08-13T12:19:18.278Z","schemaVersion":2},"datasetVersion":"2026-08-13T14:17:21.547Z"}