From 1c587edaa88be0adf133d7d78bde852a8251a3a1 Mon Sep 17 00:00:00 2001 From: Leonid Pospelov Date: Fri, 10 Jan 2025 23:50:51 +0500 Subject: [PATCH] refact(skymp5-server): replace Axios with fetch-retry in login.ts (#2273) --- skymp5-server/package.json | 2 + skymp5-server/ts/systems/login.ts | 86 +++++++++++++++++++++---------- skymp5-server/yarn.lock | 17 ++++++ 3 files changed, 78 insertions(+), 27 deletions(-) diff --git a/skymp5-server/package.json b/skymp5-server/package.json index e006f86356..fadc67b00d 100644 --- a/skymp5-server/package.json +++ b/skymp5-server/package.json @@ -10,11 +10,13 @@ "dependencies": { "@octokit/rest": "^20.0.2", "@types/lodash": "^4.14.202", + "@types/node": "^22.10.2", "argparse": "^2.0.1", "axios": "^1.7.4", "chokidar": "^3.5.3", "crc-32": "^1.2.2", "discord.js": "^14.13.0", + "fetch-retry": "^6.0.0", "koa": "^2.14.2", "koa-body": "^4.2.0", "koa-proxy": "^1.0.0-alpha.3", diff --git a/skymp5-server/ts/systems/login.ts b/skymp5-server/ts/systems/login.ts index a8026f2f72..39f9f98657 100644 --- a/skymp5-server/ts/systems/login.ts +++ b/skymp5-server/ts/systems/login.ts @@ -1,7 +1,7 @@ import { System, Log, Content, SystemContext } from "./system"; -import Axios from "axios"; import { getMyPublicIp } from "../publicIp"; import { Settings } from "../settings"; +import * as fetchRetry from "fetch-retry"; const loginFailedNotLoggedViaDiscord = JSON.stringify({ customPacketType: "loginFailedNotLoggedViaDiscord" }); const loginFailedNotInTheDiscordServer = JSON.stringify({ customPacketType: "loginFailedNotInTheDiscordServer" }); @@ -32,21 +32,40 @@ export class Login implements System { private offlineMode: boolean ) { } + private getFetchOptions(callerFunctionName: string) { + return { + // retry on any network error, or 5xx status codes + retryOn: (attempt: number, error: Error | null, response: Response) => { + const retry = error !== null || response.status >= 500; + if (retry) { + console.log(`${callerFunctionName}: retrying request ${JSON.stringify({ attempt, error, status: response.status })}`); + } + return retry; + }, + retries: 10 + }; + } + private async getUserProfile(session: string, userId: number, ctx: SystemContext): Promise { - try { - const response = await Axios.get( - `${this.masterUrl}/api/servers/${this.myAddr}/sessions/${session}` - ); - if (!response.data || !response.data.user || !response.data.user.id) { - throw new Error(`getUserProfile: bad master-api response ${JSON.stringify(response.data)}`); - } - return response.data.user as UserProfile; - } catch (error) { - if (Axios.isAxiosError(error) && error.response?.status === 404) { + const response = await this.fetchRetry( + `${this.masterUrl}/api/servers/${this.myAddr}/sessions/${session}`, + this.getFetchOptions('getUserProfile') + ); + + if (!response.ok) { + if (response.status === 404) { ctx.svr.sendCustomPacket(userId, loginFailedSessionNotFound); } - throw error; + throw new Error(`getUserProfile: HTTP error ${response.status}`); } + + const data = await response.json(); + + if (!data || !data.user || !data.user.id) { + throw new Error(`getUserProfile: bad master-api response ${JSON.stringify(data)}`); + } + + return data.user as UserProfile; } async initAsync(ctx: SystemContext): Promise { @@ -115,15 +134,17 @@ export class Login implements System { throw new Error("Not logged in via Discord"); } const guidBeforeAsyncOp = ctx.svr.getUserGuid(userId); - const response = await Axios.get( + const response = await this.fetchRetry( `https://discord.com/api/guilds/${discordAuth.guildId}/members/${profile.discordId}`, { + method: 'GET', headers: { 'Authorization': `${discordAuth.botToken}`, }, - validateStatus: (status) => true, + ... this.getFetchOptions('discordAuth1'), }, ); + const responseData = response.ok ? await response.json() : null; const guidAfterAsyncOp = ctx.svr.isConnected(userId) ? ctx.svr.getUserGuid(userId) : ""; console.log({ guidBeforeAsyncOp, guidAfterAsyncOp, op: "Discord request" }); @@ -138,11 +159,11 @@ export class Login implements System { // TODO: what if more characters const actorId = ctx.svr.getActorsByProfileId(profile.id)[0]; - const receivedRoles: string[] | null = (response.data && Array.isArray(response.data.roles)) ? response.data.roles : null; + const receivedRoles: string[] | null = (responseData && Array.isArray(responseData.roles)) ? responseData.roles : null; const currentRoles: string[] | null = actorId ? mp.get(actorId, "private.discordRoles") : null; roles = receivedRoles || currentRoles || []; - console.log('Discord request:', JSON.stringify({ status: response.status, data: response.data })); + console.log('Discord request:', JSON.stringify({ status: response.status, data: responseData })); if (discordAuth.eventLogChannelId) { let ipToPrint = ip; @@ -154,21 +175,31 @@ export class Login implements System { } const actorIds = ctx.svr.getActorsByProfileId(profile.id).map(actorId => actorId.toString(16)); - Axios.post( - `https://discord.com/api/channels/${discordAuth.eventLogChannelId}/messages`, - { + this.fetchRetry(`https://discord.com/api/channels/${discordAuth.eventLogChannelId}/messages`, { + method: 'POST', + headers: { + 'Authorization': `${discordAuth.botToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ content: `Server Login: IP ${ipToPrint}, Actor ID ${actorIds}, Master API ${profile.id}, Discord ID ${profile.discordId} <@${profile.discordId}>`, allowed_mentions: { parse: [] }, - }, - { - headers: { - 'Authorization': `${discordAuth.botToken}`, - }, - }, - ).catch((err) => console.error("Error sending message to Discord:", err)); + }), + ... this.getFetchOptions('discordAuth2'), + }) + .then((response) => { + if (!response.ok) { + throw new Error(`Error sending message to Discord: ${response.statusText}`); + } + return response.json(); + }) + .then((_data) => null) + .catch((err) => { + console.error("Error sending message to Discord:", err); + }); } - if (response.status === 404 && response.data?.code === DiscordErrors.unknownMember) { + if (response.status === 404 && responseData?.code === DiscordErrors.unknownMember) { ctx.svr.sendCustomPacket(userId, loginFailedNotInTheDiscordServer); throw new Error("Not in the Discord server"); } @@ -205,4 +236,5 @@ export class Login implements System { private myAddr: string; private settingsObject: Settings; + private fetchRetry = fetchRetry.default(global.fetch); } diff --git a/skymp5-server/yarn.lock b/skymp5-server/yarn.lock index c8f0265a89..6aa80052e2 100644 --- a/skymp5-server/yarn.lock +++ b/skymp5-server/yarn.lock @@ -358,6 +358,13 @@ dependencies: undici-types "~5.26.4" +"@types/node@^22.10.2": + version "22.10.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.2.tgz#a485426e6d1fdafc7b0d4c7b24e2c78182ddabb9" + integrity sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ== + dependencies: + undici-types "~6.20.0" + "@types/ws@8.5.9": version "8.5.9" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.9.tgz#384c489f99c83225a53f01ebc3eddf3b8e202a8c" @@ -818,6 +825,11 @@ fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== +fetch-retry@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/fetch-retry/-/fetch-retry-6.0.0.tgz#4ffdf92c834d72ae819e42a4ee2a63f1e9454426" + integrity sha512-BUFj1aMubgib37I3v4q78fYo63Po7t4HUPTpQ6/QE6yK6cIQrP+W43FYToeTEyg5m2Y7eFUtijUuAv/PDlWuag== + fill-range@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" @@ -1686,6 +1698,11 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +undici-types@~6.20.0: + version "6.20.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" + integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== + undici@5.27.2: version "5.27.2" resolved "https://registry.yarnpkg.com/undici/-/undici-5.27.2.tgz#a270c563aea5b46cc0df2550523638c95c5d4411"