From 412516d47b07c50bdd98cfeb58b1c10d97b919eb Mon Sep 17 00:00:00 2001 From: ahmedisam99 Date: Mon, 12 Oct 2026 11:54:35 +0300 Subject: [PATCH] fix(gitlab): share one token refresh between concurrent callers GitLab invalidates a refresh token when it is used. Two deployments that found the token expired at the same time sent the same refresh token, and the second one failed with "Failed to refresh token: Bad Request". --- packages/server/src/utils/providers/gitlab.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/server/src/utils/providers/gitlab.ts b/packages/server/src/utils/providers/gitlab.ts index 6cb777b75..1221fd5a4 100644 --- a/packages/server/src/utils/providers/gitlab.ts +++ b/packages/server/src/utils/providers/gitlab.ts @@ -11,7 +11,7 @@ import { TRPCError } from "@trpc/server"; import { quote } from "shell-quote"; import type { z } from "zod"; -export const refreshGitlabToken = async (gitlabProviderId: string) => { +const refreshGitlabTokenIfExpired = async (gitlabProviderId: string) => { const gitlabProvider = await findGitlabById(gitlabProviderId); const currentTime = Math.floor(Date.now() / 1000); @@ -56,6 +56,23 @@ export const refreshGitlabToken = async (gitlabProviderId: string) => { return data; }; +// GitLab invalidates a refresh token when it is used, so concurrent callers must share one refresh +const pendingRefreshes = new Map< + string, + ReturnType +>(); + +export const refreshGitlabToken = (gitlabProviderId: string) => { + let refresh = pendingRefreshes.get(gitlabProviderId); + if (!refresh) { + refresh = refreshGitlabTokenIfExpired(gitlabProviderId).finally(() => { + pendingRefreshes.delete(gitlabProviderId); + }); + pendingRefreshes.set(gitlabProviderId, refresh); + } + return refresh; +}; + export const haveGitlabRequirements = (gitlabProvider: Gitlab) => { return !!(gitlabProvider?.accessToken && gitlabProvider?.refreshToken); };