mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-08-24 21:17:31 +00:00
fix: retry qr code login polling for known transient gateway responses
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
import userStore from '$lib/stores/user-store';
|
||||
import type { DeviceLoginRequest } from '$lib/types/device-login.type';
|
||||
import { getAxiosErrorMessage } from '$lib/utils/error-util';
|
||||
import { isAxiosError } from 'axios';
|
||||
import { mode } from 'mode-watcher';
|
||||
import { onMount } from 'svelte';
|
||||
import LoginLogoErrorSuccessIndicator from '../../components/login-logo-error-success-indicator.svelte';
|
||||
@@ -37,6 +38,7 @@
|
||||
|
||||
async function startRequest() {
|
||||
stopRequest();
|
||||
// Track this attempt so stale responses cannot update the current page
|
||||
const controller = new AbortController();
|
||||
requestController = controller;
|
||||
request = undefined;
|
||||
@@ -45,12 +47,14 @@
|
||||
|
||||
try {
|
||||
request = await deviceLoginService.createRequest(controller.signal);
|
||||
// Ignore a response from an attempt replaced while the request was in flight
|
||||
if (requestController !== controller) return;
|
||||
schedulePoll(controller);
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted) return;
|
||||
errorMessage = getAxiosErrorMessage(error);
|
||||
} finally {
|
||||
// Stop the initial loading state only if this attempt still owns the page
|
||||
if (requestController === controller) {
|
||||
isStarting = false;
|
||||
}
|
||||
@@ -58,15 +62,19 @@
|
||||
}
|
||||
|
||||
function schedulePoll(controller: AbortController) {
|
||||
// Ignore schedules from stale attempts or before request metadata exists
|
||||
if (!request || requestController !== controller) return;
|
||||
// Delay the next exchange so the client respects the server-provided polling interval
|
||||
pollTimer = setTimeout(() => void exchangeRequest(controller), request.interval * 1000);
|
||||
}
|
||||
|
||||
async function exchangeRequest(controller: AbortController) {
|
||||
// Ignore work from a stopped or superseded attempt
|
||||
if (!request || requestController !== controller) return;
|
||||
|
||||
try {
|
||||
const user = await deviceLoginService.exchangeRequest(request.id, controller.signal);
|
||||
// Ignore a response from an attempt replaced while the exchange was in flight
|
||||
if (requestController !== controller) return;
|
||||
if (!user) {
|
||||
schedulePoll(controller);
|
||||
@@ -75,22 +83,42 @@
|
||||
|
||||
clearTimers();
|
||||
await userStore.setUser(user);
|
||||
// Navigate only after confirming that this attempt is still active
|
||||
if (requestController !== controller) return;
|
||||
await goto(data.redirect);
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted) return;
|
||||
// Retry transport and proxy failures because the device login request remains pending
|
||||
if (isRetryableDeviceLoginExchangeError(error)) {
|
||||
schedulePoll(controller);
|
||||
return;
|
||||
}
|
||||
// Stop retrying when the server reports a non-transient failure
|
||||
clearTimers();
|
||||
errorMessage = getAxiosErrorMessage(error);
|
||||
}
|
||||
}
|
||||
|
||||
function isRetryableDeviceLoginExchangeError(error: unknown) {
|
||||
// Ignore non-Axios errors and intentional cancellations
|
||||
if (!isAxiosError(error) || error.code === 'ERR_CANCELED') return false;
|
||||
|
||||
const retryableExchangeStatuses = new Set([408, 425, 429, 502, 503, 504, 522, 524]);
|
||||
// Retry failures without an HTTP response or known transient gateway responses
|
||||
return (
|
||||
error.response?.status === undefined || retryableExchangeStatuses.has(error.response.status)
|
||||
);
|
||||
}
|
||||
|
||||
function stopRequest() {
|
||||
// Abort the active exchange and clear its timer before replacing or leaving it
|
||||
requestController?.abort();
|
||||
requestController = undefined;
|
||||
clearTimers();
|
||||
}
|
||||
|
||||
function clearTimers() {
|
||||
// Ensure an old scheduled poll cannot start after the request lifecycle ends
|
||||
if (pollTimer) clearTimeout(pollTimer);
|
||||
pollTimer = undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user