diff --git a/src/cli/util/http-server.ts b/src/cli/util/http-server.ts index c57b88b..731dc28 100644 --- a/src/cli/util/http-server.ts +++ b/src/cli/util/http-server.ts @@ -1,10 +1,13 @@ import createDebug from 'debug'; import { NextFunction, Request, RequestHandler, Response } from 'express'; import { ErrorResponse } from '../../api/util.js'; +import { temporary_http_errors, temporary_system_errors } from '../../util/errors.js'; const debug = createDebug('cli:util:http-server'); export class HttpServer { + retry_after = 60; + protected createApiRequestHandler(callback: (req: Request, res: Response) => Promise<{} | void>, auth = false) { return async (req: Request, res: Response) => { try { @@ -42,13 +45,17 @@ export class HttpServer { return JSON.stringify(data, null, space); } - protected handleRequestError(req: Request, res: Response, err: unknown) { + protected handleRequestError(req: Request, res: Response, err: unknown, retry_after = this.retry_after) { debug('Error in request %s %s', req.method, req.url, err); if (err instanceof ErrorResponse) { - const retry_after = err.response.headers.get('Retry-After'); + const response_retry_after = err.response.headers.get('Retry-After'); - if (retry_after && /^\d+$/.test(retry_after)) { + if (response_retry_after && /^\d+$/.test(response_retry_after)) { + res.setHeader('Retry-After', response_retry_after); + } + + if (temporary_http_errors.includes(err.response.status) && !response_retry_after) { res.setHeader('Retry-After', retry_after); } } @@ -56,8 +63,8 @@ export class HttpServer { if (err && typeof err === 'object' && 'type' in err && 'code' in err && (err as any).type === 'system') { const code: string = (err as any).code; - if (code === 'ETIMEDOUT' || code === 'ENOTFOUND' || code === 'EAI_AGAIN') { - res.setHeader('Retry-After', '60'); + if (code in temporary_system_errors) { + res.setHeader('Retry-After', retry_after); } } diff --git a/src/common/notify.ts b/src/common/notify.ts index 118338f..3b01e38 100644 --- a/src/common/notify.ts +++ b/src/common/notify.ts @@ -7,8 +7,9 @@ import { ErrorResponse } from '../api/util.js'; import { SavedToken } from './auth/coral.js'; import { SplatNet2RecordsMonitor } from './splatnet2/monitor.js'; import Loop, { LoopResult } from '../util/loop.js'; -import { getTitleIdFromEcUrl, hrduration, TemporaryErrorSymbol } from '../util/misc.js'; +import { getTitleIdFromEcUrl, hrduration } from '../util/misc.js'; import { CoralUser } from './users.js'; +import { handleError } from '../util/errors.js'; const debug = createDebug('nxapi:nso:notify'); const debugFriends = createDebug('nxapi:nso:notify:friends'); @@ -183,35 +184,6 @@ export class ZncNotifications extends Loop { } } -export async function handleError( - err: ErrorResponse | NodeJS.ErrnoException, - loop: Loop, -): Promise { - if (TemporaryErrorSymbol in err && err[TemporaryErrorSymbol]) { - debug('Temporary error, waiting %ds before retrying', loop.update_interval, err); - - return LoopResult.OK; - } else if ('code' in err && (err as any).type === 'system' && err.code === 'ETIMEDOUT') { - debug('Request timed out, waiting %ds before retrying', loop.update_interval, err); - - return LoopResult.OK; - } else if ('code' in err && (err as any).type === 'system' && err.code === 'ENOTFOUND') { - debug('Request error, waiting %ds before retrying', loop.update_interval, err); - - return LoopResult.OK; - } else if ('code' in err && (err as any).type === 'system' && err.code === 'EAI_AGAIN') { - debug('Request error - name resolution failed, waiting %ds before retrying', loop.update_interval, err); - - return LoopResult.OK; - } else if ('code' in err && (err as any).type === 'system' && err.code === 'ECONNRESET') { - debug('Request error - connection reset, waiting %ds before retrying', loop.update_interval, err); - - return LoopResult.OK; - } else { - throw err; - } -} - export class NotificationManager { onPresenceUpdated?(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, type?: PresenceEvent, naid?: string, ir?: boolean): void; diff --git a/src/common/presence.ts b/src/common/presence.ts index aedcb21..bdc975c 100644 --- a/src/common/presence.ts +++ b/src/common/presence.ts @@ -3,7 +3,7 @@ import EventSource from 'eventsource'; import { DiscordRpcClient, findDiscordRpcClient } from '../discord/rpc.js'; import { getDiscordPresence, getInactiveDiscordPresence } from '../discord/util.js'; import { DiscordPresencePlayTime, DiscordPresenceContext, DiscordPresence, ExternalMonitorConstructor, ExternalMonitor, ErrorResult } from '../discord/types.js'; -import { EmbeddedSplatNet2Monitor, handleError, ZncNotifications } from './notify.js'; +import { EmbeddedSplatNet2Monitor, ZncNotifications } from './notify.js'; import { getPresenceFromUrl } from '../api/znc-proxy.js'; import { ActiveEvent, CurrentUser, Friend, Game, Presence, PresenceState, CoralErrorResponse } from '../api/coral-types.js'; import { ErrorResponse, ResponseSymbol } from '../api/util.js'; @@ -12,6 +12,7 @@ import { getTitleIdFromEcUrl } from '../index.js'; import { parseLinkHeader } from '../util/http.js'; import { getUserAgent } from '../util/useragent.js'; import { TemporaryErrorSymbol } from '../util/misc.js'; +import { handleError } from '../util/errors.js'; const debug = createDebug('nxapi:nso:presence'); const debugEventStream = createDebug('nxapi:nso:presence:sse'); diff --git a/src/util/errors.ts b/src/util/errors.ts new file mode 100644 index 0000000..a368fd8 --- /dev/null +++ b/src/util/errors.ts @@ -0,0 +1,42 @@ +import createDebug from 'debug'; +import Loop, { LoopResult } from './loop.js'; +import { CoralErrorResponse } from '../api/coral-types.js'; +import { ErrorResponse } from '../api/util.js'; +import { TemporaryErrorSymbol } from './misc.js'; + +const debug = createDebug('nxapi:util:errors'); + +export const temporary_system_errors = { + 'ETIMEDOUT': 'request timed out', + 'ENOTFOUND': null, + 'EAI_AGAIN': 'name resolution failed', + 'ECONNRESET': 'connection reset', +}; +export const temporary_http_errors = [ + 502, // Bad Gateway + 503, // Service Unavailable + 504, // Gateway Timeout +]; + +export async function handleError( + err: ErrorResponse | NodeJS.ErrnoException, + loop: Loop, +): Promise { + if (TemporaryErrorSymbol in err && err[TemporaryErrorSymbol]) { + debug('Temporary error, waiting %ds before retrying', loop.update_interval, err); + + return LoopResult.OK; + } else if ('code' in err && (err as any).type === 'system' && err.code && err.code in temporary_system_errors) { + const desc = temporary_system_errors[err.code as keyof typeof temporary_system_errors]; + debug('Request error%s, waiting %ds before retrying', desc ? ' - ' + desc : '', loop.update_interval, err); + + return LoopResult.OK; + } else if (err instanceof ErrorResponse && temporary_http_errors.includes(err.response.status)) { + debug('Request error - HTTP %s (%s), waiting %ds before retrying', + err.response.status, err.response.statusText, loop.update_interval, err); + + return LoopResult.OK; + } else { + throw err; + } +}