fix: retry login across deploy cutovers
This commit is contained in:
1 parent
bfc951cfd9
commit
a57c73055f
4 files changed
+94
-12
No files matched your search
@@ -8,6 +8,7 @@ import {
|
||||
CaptchaWidget,
|
||||
readCaptchaToken,
|
||||
} from "@/components/auth/captcha-widget";
|
||||
import { signInWithTransientRetry } from "@/lib/auth/sign-in-retry";
|
||||
|
||||
export function HomeLoginForm({
|
||||
captcha = { provider: "none" },
|
||||
@@ -49,12 +50,14 @@ export function HomeLoginForm({
|
||||
return;
|
||||
}
|
||||
}
|
||||
const res = await signIn("credentials", {
|
||||
username,
|
||||
password,
|
||||
code,
|
||||
redirect: false,
|
||||
});
|
||||
const res = await signInWithTransientRetry(() =>
|
||||
signIn("credentials", {
|
||||
username,
|
||||
password,
|
||||
code,
|
||||
redirect: false,
|
||||
}),
|
||||
);
|
||||
if (!res || res.error) {
|
||||
setError(
|
||||
needs2fa ? "Invalid 2FA code" : "Invalid username or password",
|
||||
@@ -62,6 +65,8 @@ export function HomeLoginForm({
|
||||
return;
|
||||
}
|
||||
window.location.href = "/";
|
||||
} catch {
|
||||
setError(needs2fa ? "Invalid 2FA code" : "Invalid username or password");
|
||||
} finally {
|
||||
setPending(false);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
CaptchaWidget,
|
||||
readCaptchaToken,
|
||||
} from "@/components/auth/captcha-widget";
|
||||
import { signInWithTransientRetry } from "@/lib/auth/sign-in-retry";
|
||||
|
||||
export function LoginForm({
|
||||
captcha = { provider: "none" },
|
||||
@@ -52,12 +53,14 @@ export function LoginForm({
|
||||
return;
|
||||
}
|
||||
}
|
||||
const res = await signIn("credentials", {
|
||||
username,
|
||||
password,
|
||||
code,
|
||||
redirect: false,
|
||||
});
|
||||
const res = await signInWithTransientRetry(() =>
|
||||
signIn("credentials", {
|
||||
username,
|
||||
password,
|
||||
code,
|
||||
redirect: false,
|
||||
}),
|
||||
);
|
||||
if (!res || res.error) {
|
||||
setError(
|
||||
needs2fa ? t("errorInvalid2fa") : t("errorInvalidCredentials"),
|
||||
@@ -65,6 +68,8 @@ export function LoginForm({
|
||||
return;
|
||||
}
|
||||
window.location.href = "/me";
|
||||
} catch {
|
||||
setError(needs2fa ? t("errorInvalid2fa") : t("errorInvalidCredentials"));
|
||||
} finally {
|
||||
setPending(false);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { signInWithTransientRetry } from "./sign-in-retry";
|
||||
|
||||
describe("signInWithTransientRetry", () => {
|
||||
it("retries thrown transport errors with a short backoff", async () => {
|
||||
const transportError = new SyntaxError("unexpected character in JSON");
|
||||
const signIn = vi
|
||||
.fn<() => Promise<{ ok: boolean }>>()
|
||||
.mockRejectedValueOnce(transportError)
|
||||
.mockRejectedValueOnce(transportError)
|
||||
.mockResolvedValue({ ok: true });
|
||||
const wait = vi.fn(async () => undefined);
|
||||
|
||||
await expect(signInWithTransientRetry(signIn, wait)).resolves.toEqual({
|
||||
ok: true,
|
||||
});
|
||||
expect(signIn).toHaveBeenCalledTimes(3);
|
||||
expect(wait).toHaveBeenNthCalledWith(1, 400);
|
||||
expect(wait).toHaveBeenNthCalledWith(2, 800);
|
||||
});
|
||||
|
||||
it("returns ordinary auth responses without retrying", async () => {
|
||||
const response = { ok: false, error: "CredentialsSignin" };
|
||||
const signIn = vi.fn(async () => response);
|
||||
const wait = vi.fn(async () => undefined);
|
||||
|
||||
await expect(signInWithTransientRetry(signIn, wait)).resolves.toBe(
|
||||
response,
|
||||
);
|
||||
expect(signIn).toHaveBeenCalledOnce();
|
||||
expect(wait).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rethrows the last transport error after four attempts", async () => {
|
||||
const transportError = new Error("temporary upstream failure");
|
||||
const signIn = vi
|
||||
.fn<() => Promise<never>>()
|
||||
.mockRejectedValue(transportError);
|
||||
const wait = vi.fn(async () => undefined);
|
||||
|
||||
await expect(signInWithTransientRetry(signIn, wait)).rejects.toBe(
|
||||
transportError,
|
||||
);
|
||||
expect(signIn).toHaveBeenCalledTimes(4);
|
||||
expect(wait).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
type Wait = (delayMs: number) => Promise<unknown>;
|
||||
|
||||
const waitForDelay: Wait = (delayMs) =>
|
||||
new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
|
||||
export async function signInWithTransientRetry<T>(
|
||||
signIn: () => Promise<T>,
|
||||
wait: Wait = waitForDelay,
|
||||
): Promise<T> {
|
||||
const maxAttempts = 4;
|
||||
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||
try {
|
||||
return await signIn();
|
||||
} catch (error) {
|
||||
if (attempt === maxAttempts - 1) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
await wait(400 * 2 ** attempt);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Unreachable auth retry state");
|
||||
}
|
||||
Reference in new issue
Block a user