Devolutions/UniGetUI · warning · InvalidOperationException

The pending GitHub device flow has expired. Start sign-in ag

Error message

The pending GitHub device flow has expired. Start sign-in again.

What it means

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.

Source

Thrown at src/UniGetUI.Interface.IpcApi/IpcBackupApi.cs:211

        {
            Status = "success",
            Command = "start-github-sign-in",
            Message = request.LaunchBrowser
                ? "GitHub device flow started and the verification page was opened."
                : "GitHub device flow started.",
            Auth = await GetGitHubAuthInfoAsync(),
        };
    }

    public static async Task<IpcGitHubAuthResult> CompleteGitHubDeviceFlowAsync()
    {
        EnsureGitHubClientConfigured();

        PendingGitHubDeviceFlow pending = GetPendingGitHubDeviceFlow();
        if (DateTimeOffset.UtcNow >= pending.ExpiresAtUtc)
        {
            ClearPendingGitHubDeviceFlow();
            throw new InvalidOperationException(
                "The pending GitHub device flow has expired. Start sign-in again."
            );
        }

        try
        {
            using var client = CreateAnonymousGitHubClient();
            var token = await client.CreateAccessTokenForDeviceFlowAsync(
                Secrets.GetGitHubClientId(),
                pending.DeviceFlow,
                CancellationToken.None
            );

            if (string.IsNullOrWhiteSpace(token.AccessToken))
            {
                throw new InvalidOperationException("GitHub did not return an access token.");
            }

View on GitHub (pinned to 9b1d7d0eab)

Solutions

  1. Call StartGitHubDeviceFlowAsync again to obtain a fresh device code, then complete it within the new ExpiresIn window.
  2. Poll CompleteGitHubDeviceFlowAsync at the interval returned by the device flow (PollIntervalSeconds) to avoid missing the window.
  3. Check the ExpiresAt timestamp from IpcGitHubAuthInfo and surface a countdown to the user so they act in time.

Example fix

// before: completing after the code expired
await IpcBackupApi.CompleteGitHubDeviceFlowAsync();
// after: restart the flow when expired
var auth = await IpcBackupApi.GetStatusAsync();
if (auth.Auth.DeviceFlowPending && auth.Auth.ExpiresAt > DateTimeOffset.UtcNow)
    await IpcBackupApi.CompleteGitHubDeviceFlowAsync();
else
    await IpcBackupApi.StartGitHubDeviceFlowAsync(new IpcGitHubDeviceFlowRequest { LaunchBrowser = true });
Defensive patterns

Strategy: validation

Validate before calling

var auth = await IpcBackupApi.GetStatusAsync();
if (auth.Auth.DeviceFlowPending && auth.Auth.ExpiresAt > DateTimeOffset.UtcNow)
    await IpcBackupApi.CompleteGitHubDeviceFlowAsync();
else
    await IpcBackupApi.StartGitHubDeviceFlowAsync(new IpcGitHubDeviceFlowRequest { LaunchBrowser = true });

Type guard

static bool DeviceFlowStillValid(IpcGitHubAuthInfo auth) =>
    auth.DeviceFlowPending && auth.ExpiresAt is { } exp && DateTimeOffset.UtcNow < exp;

Try / catch

try { await IpcBackupApi.CompleteGitHubDeviceFlowAsync(); }
catch (InvalidOperationException ex) when (ex.Message.Contains("expired"))
{ await IpcBackupApi.StartGitHubDeviceFlowAsync(new IpcGitHubDeviceFlowRequest { LaunchBrowser = true }); }

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of Devolutions/UniGetUI@9b1d7d0eab (2026-08-13). Data as JSON: /api/errors/dff64d8f2c1d233c. Report an issue: GitHub.