API error classes

This commit is contained in:
Samuel Elliott 2023-07-04 22:01:22 +01:00
parent 5bd62cd576
commit 3c6025790e
No known key found for this signature in database
GPG Key ID: 8420C7CDE43DC4D6
25 changed files with 217 additions and 126 deletions

View File

@ -5,7 +5,7 @@ export interface CoralSuccessResponse<T = unknown> {
correlationId: string;
}
export interface CoralErrorResponse {
export interface CoralError {
status: CoralStatus | number;
errorMessage: string;
correlationId: string;
@ -53,7 +53,7 @@ export enum CoralStatus {
// UNKNOWN = -1,
}
export type CoralResponse<T = unknown> = CoralSuccessResponse<T> | CoralErrorResponse;
export type CoralResponse<T = unknown> = CoralSuccessResponse<T> | CoralError;
export interface AccountLoginParameter {
naIdToken: string;

View File

@ -5,7 +5,7 @@ import { JwtPayload } from '../util/jwt.js';
import { timeoutSignal } from '../util/misc.js';
import { getAdditionalUserAgents } from '../util/useragent.js';
import type { CoralRemoteConfig } from '../common/remote-config.js';
import { AccountLogin, AccountLoginParameter, AccountToken, AccountTokenParameter, Announcements, CoralErrorResponse, CoralResponse, CoralStatus, CoralSuccessResponse, CurrentUser, CurrentUserPermissions, Event, FriendCodeUrl, FriendCodeUser, Friends, GetActiveEventResult, PresencePermissions, User, WebServices, WebServiceToken, WebServiceTokenParameter } from './coral-types.js';
import { AccountLogin, AccountLoginParameter, AccountToken, AccountTokenParameter, Announcements, CoralError, CoralResponse, CoralStatus, CoralSuccessResponse, CurrentUser, CurrentUserPermissions, Event, FriendCodeUrl, FriendCodeUser, Friends, GetActiveEventResult, PresencePermissions, User, WebServices, WebServiceToken, WebServiceTokenParameter } from './coral-types.js';
import { f, FResult, HashMethod } from './f.js';
import { generateAuthData, getNintendoAccountToken, getNintendoAccountUser, NintendoAccountSessionAuthorisation, NintendoAccountToken, NintendoAccountUser } from './na.js';
import { ErrorResponse, ResponseSymbol } from './util.js';
@ -74,7 +74,7 @@ export default class CoralApi implements CoralApiInterface {
[CoralUserIdSymbol]: string;
[NintendoAccountIdSymbol]: string;
onTokenExpired: ((data?: CoralErrorResponse, res?: Response) => Promise<CoralAuthData | void>) | null = null;
onTokenExpired: ((data?: CoralError, res?: Response) => Promise<CoralAuthData | void>) | null = null;
/** @internal */
_renewToken: Promise<void> | null = null;
/** @internal */
@ -144,7 +144,7 @@ export default class CoralApi implements CoralApiInterface {
debug('fetch %s %s, response %s', method, url, response.status);
if (response.status !== 200) {
throw new ErrorResponse('[znc] Non-200 status code', response, await response.text());
throw new CoralErrorResponse('[znc] Non-200 status code', response, await response.text());
}
const data = await response.json() as CoralResponse<T>;
@ -161,10 +161,10 @@ export default class CoralApi implements CoralApiInterface {
}
if ('errorMessage' in data) {
throw new ErrorResponse('[znc] ' + data.errorMessage, response, data);
throw new CoralErrorResponse('[znc] ' + data.errorMessage, response, data);
}
if (data.status !== CoralStatus.OK) {
throw new ErrorResponse('[znc] Unknown error', response, data);
throw new CoralErrorResponse('[znc] Unknown error', response, data);
}
const result = data.result;
@ -295,7 +295,7 @@ export default class CoralApi implements CoralApiInterface {
try {
return await this.call<WebServiceToken>('/v2/Game/GetWebServiceToken', req, false);
} catch (err) {
if (err instanceof ErrorResponse && err.data.status === CoralStatus.TOKEN_EXPIRED && !_attempt && this.onTokenExpired) {
if (err instanceof CoralErrorResponse && err.status === CoralStatus.TOKEN_EXPIRED && !_attempt && this.onTokenExpired) {
debug('Error getting web service token, renewing token before retrying', err);
// _renewToken will be awaited when calling getWebServiceToken
this._renewToken = this._renewToken ?? this.onTokenExpired.call(null, err.data, err.response as Response).then(data => {
@ -448,16 +448,16 @@ export default class CoralApi implements CoralApiInterface {
debug('fetch %s %s, response %s', 'POST', '/v3/Account/Login', response.status);
if (response.status !== 200) {
throw new ErrorResponse('[znc] Non-200 status code', response, await response.text());
throw new CoralErrorResponse('[znc] Non-200 status code', response, await response.text());
}
const data = await response.json() as CoralResponse<AccountLogin>;
if ('errorMessage' in data) {
throw new ErrorResponse('[znc] ' + data.errorMessage, response, data);
throw new CoralErrorResponse('[znc] ' + data.errorMessage, response, data);
}
if (data.status !== CoralStatus.OK) {
throw new ErrorResponse('[znc] Unknown error', response, data);
throw new CoralErrorResponse('[znc] Unknown error', response, data);
}
debug('Got Nintendo Switch Online app token', data);
@ -474,6 +474,12 @@ export default class CoralApi implements CoralApiInterface {
}
}
export class CoralErrorResponse extends ErrorResponse<CoralError> {
get status(): CoralStatus | null {
return this.data?.status ?? null;
}
}
const na_client_settings = {
client_id: ZNCA_CLIENT_ID,
scope: 'openid user user.birthday user.mii user.screenName',

View File

@ -86,13 +86,13 @@ export default class MoonApi {
}
if (response.status !== 200) {
throw new ErrorResponse('[moon] Non-200 status code', response, await response.text());
throw new MoonErrorResponse('[moon] Non-200 status code', response, await response.text());
}
const data = await response.json() as T | MoonError;
if ('errorCode' in data) {
throw new ErrorResponse('[moon] ' + data.title, response, data);
throw new MoonErrorResponse('[moon] ' + data.title, response, data);
}
return defineResponse(data, response);
@ -177,6 +177,8 @@ export default class MoonApi {
}
}
export class MoonErrorResponse extends ErrorResponse<MoonError> {}
const na_client_settings = {
client_id: ZNMA_CLIENT_ID,
scope: [

View File

@ -126,16 +126,16 @@ export async function getNintendoAccountSessionToken(code: string, verifier: str
}).finally(cancel);
if (response.status !== 200) {
throw new ErrorResponse('[na] Non-200 status code', response, await response.text());
throw new NintendoAccountAuthErrorResponse('[na] Non-200 status code', response, await response.text());
}
const token = await response.json() as NintendoAccountSessionToken | NintendoAccountAuthError | NintendoAccountError;
if ('errorCode' in token) {
throw new ErrorResponse<NintendoAccountError>('[na] ' + token.detail, response, token);
}
if ('error' in token) {
throw new ErrorResponse<NintendoAccountAuthError>('[na] ' + token.error_description ?? token.error, response, token);
throw new NintendoAccountAuthErrorResponse('[na] ' + token.error_description ?? token.error, response, token);
}
if ('errorCode' in token) {
throw new NintendoAccountErrorResponse('[na] ' + token.detail, response, token);
}
debug('Got Nintendo Account session token', token);
@ -163,16 +163,17 @@ export async function getNintendoAccountToken(token: string, client_id: string)
}).finally(cancel);
if (response.status !== 200) {
throw new ErrorResponse('[na] Non-200 status code', response, await response.text());
throw new NintendoAccountAuthErrorResponse('[na] Non-200 status code', response, await response.text());
}
const nintendoAccountToken = await response.json() as NintendoAccountToken | NintendoAccountAuthError | NintendoAccountError;
if ('errorCode' in nintendoAccountToken) {
throw new ErrorResponse<NintendoAccountError>('[na] ' + nintendoAccountToken.detail, response, nintendoAccountToken);
}
if ('error' in nintendoAccountToken) {
throw new ErrorResponse<NintendoAccountAuthError>('[na] ' + nintendoAccountToken.error_description ?? nintendoAccountToken.error, response, nintendoAccountToken);
throw new NintendoAccountAuthErrorResponse('[na] ' + nintendoAccountToken.error_description ??
nintendoAccountToken.error, response, nintendoAccountToken);
}
if ('errorCode' in nintendoAccountToken) {
throw new NintendoAccountErrorResponse('[na] ' + nintendoAccountToken.detail, response, nintendoAccountToken);
}
debug('Got Nintendo Account token', nintendoAccountToken);
@ -196,13 +197,13 @@ export async function getNintendoAccountUser(token: NintendoAccountToken) {
}).finally(cancel);
if (response.status !== 200) {
throw new ErrorResponse('[na] Non-200 status code', response, await response.text());
throw new NintendoAccountErrorResponse('[na] Non-200 status code', response, await response.text());
}
const user = await response.json() as NintendoAccountUser | NintendoAccountError;
if ('errorCode' in user) {
throw new ErrorResponse<NintendoAccountError>('[na] ' + user.detail, response, user);
throw new NintendoAccountErrorResponse('[na] ' + user.detail, response, user);
}
debug('Got Nintendo Account user info', user);
@ -406,3 +407,7 @@ export interface NintendoAccountError {
status: number;
type: string;
}
export class NintendoAccountAuthErrorResponse extends ErrorResponse<NintendoAccountAuthError> {}
export class NintendoAccountErrorResponse extends ErrorResponse<NintendoAccountError> {}

View File

@ -80,14 +80,14 @@ export default class NooklinkApi {
return this.fetch(url, method, body, headers, _autoRenewToken, _attempt + 1);
}
if (response.status !== 200 && response.status !== 201) {
throw new ErrorResponse('[nooklink] Non-200/201 status code', response, await response.text());
if (!response.ok) {
throw new NooklinkErrorResponse('[nooklink] Non-2xx status code', response, await response.text());
}
const data = await response.json() as T | WebServiceError;
if ('code' in data) {
throw new ErrorResponse<WebServiceError>('[nooklink] Error ' + data.code, response, data);
throw new NooklinkErrorResponse('[nooklink] Error ' + data.code, response, data);
}
return defineResponse(data, response);
@ -175,14 +175,14 @@ export default class NooklinkApi {
const body = await response.text();
if (response.status !== 200) {
throw new ErrorResponse('[nooklink] Non-200 status code', response, body);
throw new NooklinkErrorResponse('[nooklink] Non-200 status code', response, body);
}
const cookies = response.headers.get('Set-Cookie');
const match = cookies?.match(/\b_gtoken=([^;]*)(;(\s*((?!expires)[a-z]+=([^;]*));?)*(\s*(expires=([^;]*));?)?|$)/i);
if (!match) {
throw new ErrorResponse('[nooklink] Response didn\'t include _gtoken cookie', response, body);
throw new NooklinkErrorResponse('[nooklink] Response didn\'t include _gtoken cookie', response, body);
}
const gtoken = decodeURIComponent(match[1]);
@ -275,14 +275,14 @@ export class NooklinkUserApi {
return this.fetch(url, method, body, headers, _autoRenewToken, _attempt + 1);
}
if (response.status !== 200 && response.status !== 201) {
throw new ErrorResponse('[nooklink] Non-200/201 status code', response, await response.text());
if (!response.ok) {
throw new NooklinkErrorResponse('[nooklink] Non-2xx status code', response, await response.text());
}
const data = await response.json() as T | WebServiceError;
if ('code' in data) {
throw new ErrorResponse<WebServiceError>('[nooklink] Error ' + data.code, response, data);
throw new NooklinkErrorResponse('[nooklink] Error ' + data.code, response, data);
}
return defineResponse(data, response);
@ -391,6 +391,8 @@ export class NooklinkUserApi {
}
}
export class NooklinkErrorResponse extends ErrorResponse<WebServiceError> {}
export interface NooklinkAuthData {
webserviceToken: WebServiceToken;
url: string;

View File

@ -63,7 +63,7 @@ export default class SplatNet2Api {
}
if (response.status !== 200) {
throw new ErrorResponse('[splatnet2] Non-200 status code', response, await response.text());
throw new SplatNet2ErrorResponse('[splatnet2] Non-200 status code', response, await response.text());
}
updateIksmSessionLastUsed.handler?.call(null, this.iksm_session);
@ -71,7 +71,7 @@ export default class SplatNet2Api {
const data = await response.json() as T | WebServiceError;
if ('code' in data) {
throw new ErrorResponse<WebServiceError>('[splatnet2] ' + data.message, response, data);
throw new SplatNet2ErrorResponse('[splatnet2] ' + data.message, response, data);
}
return defineResponse(data, response);
@ -305,14 +305,14 @@ ${colour}
const body = await response.text();
if (response.status !== 200) {
throw new ErrorResponse('[splatnet2] Non-200 status code', response, body);
throw new SplatNet2ErrorResponse('[splatnet2] Non-200 status code', response, body);
}
const cookies = response.headers.get('Set-Cookie');
const match = cookies?.match(/\biksm_session=([^;]*)(;(\s*((?!expires)[a-z]+=([^;]*));?)*(\s*(expires=([^;]*));?)?|$)/i);
if (!match) {
throw new ErrorResponse('[splatnet2] Response didn\'t include iksm_session cookie', response, body);
throw new SplatNet2ErrorResponse('[splatnet2] Response didn\'t include iksm_session cookie', response, body);
}
const iksm_session = decodeURIComponent(match[1]);
@ -330,10 +330,10 @@ ${colour}
const mn = body.match(/<html(?:\s+[a-z0-9-]+(?:=(?:"[^"]*"|[^\s>]*))?)*\s+data-nsa-id=(?:"([^"]*)"|([^\s>]*))/i);
const [language, region, user_id, nsa_id] = [ml, mr, mu, mn].map(m => m?.[1] || m?.[2] || null);
if (!language) throw new Error('[splatnet2] Invalid language in response');
if (!region) throw new Error('[splatnet2] Invalid region in response');
if (!user_id) throw new Error('[splatnet2] Invalid unique player ID in response');
if (!nsa_id) throw new Error('[splatnet2] Invalid NSA ID in response');
if (!language) throw new ErrorResponse('[splatnet2] Invalid language in response', response, body);
if (!region) throw new ErrorResponse('[splatnet2] Invalid region in response', response, body);
if (!user_id) throw new ErrorResponse('[splatnet2] Invalid unique player ID in response', response, body);
if (!nsa_id) throw new ErrorResponse('[splatnet2] Invalid NSA ID in response', response, body);
debug('SplatNet 2 user', {
language,
@ -359,6 +359,8 @@ ${colour}
}
}
export class SplatNet2ErrorResponse extends ErrorResponse<WebServiceError> {}
export interface SplatNet2AuthData {
webserviceToken: WebServiceToken;
url: string;

View File

@ -1,5 +1,5 @@
import fetch, { Response } from 'node-fetch';
import { BankaraBattleHistoriesRefetchResult, BankaraBattleHistoriesRefetchVariables, GraphQLRequest, GraphQLResponse, GraphQLSuccessResponse, KnownRequestId, LatestBattleHistoriesRefetchResult, LatestBattleHistoriesRefetchVariables, MyOutfitInput, PagerUpdateBattleHistoriesByVsModeResult, PagerUpdateBattleHistoriesByVsModeVariables, PrivateBattleHistoriesRefetchResult, PrivateBattleHistoriesRefetchVariables, RegularBattleHistoriesRefetchResult, RegularBattleHistoriesRefetchVariables, RequestId, ResultTypes, VariablesTypes, XBattleHistoriesRefetchResult, XBattleHistoriesRefetchVariables } from 'splatnet3-types/splatnet3';
import { BankaraBattleHistoriesRefetchResult, BankaraBattleHistoriesRefetchVariables, GraphQLError, GraphQLErrorResponse, GraphQLRequest, GraphQLResponse, GraphQLSuccessResponse, KnownRequestId, LatestBattleHistoriesRefetchResult, LatestBattleHistoriesRefetchVariables, MyOutfitInput, PagerUpdateBattleHistoriesByVsModeResult, PagerUpdateBattleHistoriesByVsModeVariables, PrivateBattleHistoriesRefetchResult, PrivateBattleHistoriesRefetchVariables, RegularBattleHistoriesRefetchResult, RegularBattleHistoriesRefetchVariables, RequestId, ResultTypes, VariablesTypes, XBattleHistoriesRefetchResult, XBattleHistoriesRefetchVariables } from 'splatnet3-types/splatnet3';
import { WebServiceToken } from './coral-types.js';
import { CoralApiInterface } from './coral.js';
import { NintendoAccountUser } from './na.js';
@ -27,15 +27,25 @@ const SPLATNET3_URL = SPLATNET3_WEBSERVICE_URL + '/api';
const SHOULD_RENEW_TOKEN_AT = 300; // 5 minutes in seconds
const TOKEN_EXPIRES_IN = 2 * 60 * 60 * 1000; // 2 hours in milliseconds
export enum SplatNet3AuthErrorCode {
USER_NOT_REGISTERED = 'USER_NOT_REGISTERED',
ERROR_INVALID_PARAMETERS = 'ERROR_INVALID_PARAMETERS',
ERROR_INVALID_GAME_WEB_TOKEN = 'ERROR_INVALID_GAME_WEB_TOKEN',
ERROR_OBSOLETE_VERSION = 'ERROR_OBSOLETE_VERSION',
ERROR_RATE_LIMIT = 'ERROR_RATE_LIMIT',
ERROR_SERVER = 'ERROR_SERVER',
ERROR_SERVER_MAINTENANCE = 'ERROR_SERVER_MAINTENANCE',
}
const AUTH_ERROR_CODES = {
204: 'USER_NOT_REGISTERED',
400: 'ERROR_INVALID_PARAMETERS',
401: 'ERROR_INVALID_GAME_WEB_TOKEN',
403: 'ERROR_OBSOLETE_VERSION',
429: 'ERROR_RATE_LIMIT',
500: 'ERROR_SERVER',
503: 'ERROR_SERVER_MAINTENANCE',
599: 'ERROR_SERVER',
204: SplatNet3AuthErrorCode.USER_NOT_REGISTERED,
400: SplatNet3AuthErrorCode.ERROR_INVALID_PARAMETERS,
401: SplatNet3AuthErrorCode.ERROR_INVALID_GAME_WEB_TOKEN,
403: SplatNet3AuthErrorCode.ERROR_OBSOLETE_VERSION,
429: SplatNet3AuthErrorCode.ERROR_RATE_LIMIT,
500: SplatNet3AuthErrorCode.ERROR_SERVER,
503: SplatNet3AuthErrorCode.ERROR_SERVER_MAINTENANCE,
599: SplatNet3AuthErrorCode.ERROR_SERVER,
} as const;
const REPLAY_CODE_REGEX = /^[A-Z0-9]{16}$/;
@ -143,7 +153,7 @@ export default class SplatNet3Api {
}
if (response.status !== 200) {
throw new ErrorResponse('[splatnet3] Non-200 status code', response, await response.text());
throw new SplatNet3ErrorResponse('[splatnet3] Non-200 status code', response, await response.text());
}
const remaining = parseInt(response.headers.get('x-bullettoken-remaining') ?? '0');
@ -189,9 +199,9 @@ export default class SplatNet3Api {
const data = await this.fetch<GraphQLResponse<_Result>>('/graphql', 'POST', JSON.stringify(req), undefined,
'graphql query ' + id);
if (!('data' in data) || (this.graphql_strict && data.errors?.length)) {
throw new ErrorResponse('[splatnet3] GraphQL error: ' + data.errors!.map(e => e.message).join(', '),
data[ResponseSymbol], data);
if (data.errors && (!('data' in data) || this.graphql_strict)) {
throw SplatNet3GraphQLErrorResponse.from(data[ResponseSymbol], data as GraphQLResponseWithErrors,
id, variables);
}
for (const error of data.errors ?? []) {
@ -377,7 +387,7 @@ export default class SplatNet3Api {
});
if (!result.data.journey) {
throw new ErrorResponse('[splatnet3] Journey not found', result[ResponseSymbol], result);
throw SplatNet3GraphQLResourceNotFoundResponse.from('[splatnet3] Journey not found', result);
}
return result as NotNullPersistedQueryResult<typeof result, 'journey'>;
@ -390,7 +400,7 @@ export default class SplatNet3Api {
});
if (!result.data.journey) {
throw new ErrorResponse('[splatnet3] Journey not found', result[ResponseSymbol], result);
throw SplatNet3GraphQLResourceNotFoundResponse.from('[splatnet3] Journey not found', result);
}
return result as NotNullPersistedQueryResult<typeof result, 'journey'>;
@ -403,7 +413,7 @@ export default class SplatNet3Api {
});
if (!result.data.journey) {
throw new ErrorResponse('[splatnet3] Journey not found', result[ResponseSymbol], result);
throw SplatNet3GraphQLResourceNotFoundResponse.from('[splatnet3] Journey not found', result);
}
return result as NotNullPersistedQueryResult<typeof result, 'journey'>;
@ -416,7 +426,7 @@ export default class SplatNet3Api {
});
if (!result.data.journey) {
throw new ErrorResponse('[splatnet3] Journey not found', result[ResponseSymbol], result);
throw SplatNet3GraphQLResourceNotFoundResponse.from('[splatnet3] Journey not found', result);
}
return result as NotNullPersistedQueryResult<typeof result, 'journey'>;
@ -450,7 +460,7 @@ export default class SplatNet3Api {
});
if (!result.data.fest) {
throw new ErrorResponse('[splatnet3] Fest not found', result[ResponseSymbol], result);
throw SplatNet3GraphQLResourceNotFoundResponse.from('[splatnet3] Fest not found', result);
}
return result as NotNullPersistedQueryResult<typeof result, 'fest'>;
@ -463,7 +473,7 @@ export default class SplatNet3Api {
});
if (!result.data.fest) {
throw new ErrorResponse('[splatnet3] Fest not found', result[ResponseSymbol], result);
throw SplatNet3GraphQLResourceNotFoundResponse.from('[splatnet3] Fest not found', result);
}
return result as NotNullPersistedQueryResult<typeof result, 'fest'>;
@ -476,7 +486,7 @@ export default class SplatNet3Api {
});
if (!result.data.fest) {
throw new ErrorResponse('[splatnet3] Fest not found', result[ResponseSymbol], result);
throw SplatNet3GraphQLResourceNotFoundResponse.from('[splatnet3] Fest not found', result);
}
return result as NotNullPersistedQueryResult<typeof result, 'fest'>;
@ -489,7 +499,7 @@ export default class SplatNet3Api {
});
if (!result.data.fest) {
throw new ErrorResponse('[splatnet3] Fest not found', result[ResponseSymbol], result);
throw SplatNet3GraphQLResourceNotFoundResponse.from('[splatnet3] Fest not found', result);
}
return result as NotNullPersistedQueryResult<typeof result, 'fest'>;
@ -509,7 +519,7 @@ export default class SplatNet3Api {
});
if (!result.data.fest) {
throw new ErrorResponse('[splatnet3] Fest not found', result[ResponseSymbol], result);
throw SplatNet3GraphQLResourceNotFoundResponse.from('[splatnet3] Fest not found', result);
}
return result as NotNullPersistedQueryResult<typeof result, 'fest'>;
@ -528,7 +538,7 @@ export default class SplatNet3Api {
});
if (!result.data.node) {
throw new ErrorResponse('[splatnet3] FestTeam not found', result[ResponseSymbol], result);
throw SplatNet3GraphQLResourceNotFoundResponse.from('[splatnet3] FestTeam not found', result);
}
return result as NotNullPersistedQueryResult<typeof result, 'node'>;
@ -589,7 +599,7 @@ export default class SplatNet3Api {
null :
null;
if (!query) throw new Error('Invalid leaderboard');
if (!query) throw new TypeError('Invalid leaderboard');
return this.persistedQuery<{
[XRankingLeaderboardType.X_RANKING]: {
@ -633,7 +643,7 @@ export default class SplatNet3Api {
});
if (!result.data.saleGear) {
throw new ErrorResponse('[splatnet3] Sale gear not found', result[ResponseSymbol], result);
throw SplatNet3GraphQLResourceNotFoundResponse.from('[splatnet3] Sale gear not found', result);
}
return result as NotNullPersistedQueryResult<typeof result, 'saleGear'>;
@ -670,7 +680,7 @@ export default class SplatNet3Api {
});
if (!result.data.myOutfit) {
throw new ErrorResponse('[splatnet3] My outfit not found', result[ResponseSymbol], result);
throw SplatNet3GraphQLResourceNotFoundResponse.from('[splatnet3] My outfit not found', result);
}
return result as NotNullPersistedQueryResult<typeof result, 'myOutfit'>;
@ -741,7 +751,7 @@ export default class SplatNet3Api {
});
if (!result.data.replay) {
throw new ErrorResponse('[splatnet3] Replay not found', result[ResponseSymbol], result);
throw SplatNet3GraphQLResourceNotFoundResponse.from('[splatnet3] Replay not found', result);
}
return result as NotNullPersistedQueryResult<typeof result, 'replay'>;
@ -842,7 +852,7 @@ export default class SplatNet3Api {
});
if (!result.data.vsHistoryDetail) {
throw new ErrorResponse('[splatnet3] Battle history not found', result[ResponseSymbol], result);
throw SplatNet3GraphQLResourceNotFoundResponse.from('[splatnet3] Battle history not found', result);
}
return result as NotNullPersistedQueryResult<typeof result, 'vsHistoryDetail'>;
@ -855,7 +865,7 @@ export default class SplatNet3Api {
});
if (!result.data.vsHistoryDetail) {
throw new ErrorResponse('[splatnet3] Battle history not found', result[ResponseSymbol], result);
throw SplatNet3GraphQLResourceNotFoundResponse.from('[splatnet3] Battle history not found', result);
}
return result as NotNullPersistedQueryResult<typeof result, 'vsHistoryDetail'>;
@ -901,7 +911,7 @@ export default class SplatNet3Api {
});
if (!result.data.coopHistoryDetail) {
throw new ErrorResponse('[splatnet3] Co-op history not found', result[ResponseSymbol], result);
throw SplatNet3GraphQLResourceNotFoundResponse.from('[splatnet3] Co-op history not found', result);
}
return result as NotNullPersistedQueryResult<typeof result, 'coopHistoryDetail'>;
@ -914,7 +924,7 @@ export default class SplatNet3Api {
});
if (!result.data.node) {
throw new ErrorResponse('[splatnet3] Co-op history not found', result[ResponseSymbol], result);
throw SplatNet3GraphQLResourceNotFoundResponse.from('[splatnet3] Co-op history not found', result);
}
return result as NotNullPersistedQueryResult<typeof result, 'node'>;
@ -1022,7 +1032,7 @@ export default class SplatNet3Api {
const body = await response.text();
if (response.status !== 200) {
throw new ErrorResponse('[splatnet3] Non-200 status code', response, body);
throw new SplatNet3ErrorResponse('[splatnet3] Non-200 status code', response, body);
}
const cookies = response.headers.get('Set-Cookie');
@ -1047,9 +1057,9 @@ export default class SplatNet3Api {
debug('fetch %s %s, response %s', 'POST', '/bullet_tokens', response.status);
const error: string | undefined = AUTH_ERROR_CODES[tr.status as keyof typeof AUTH_ERROR_CODES];
if (error) throw new ErrorResponse('[splatnet3] ' + error, tr, await tr.text());
if (tr.status !== 201) throw new ErrorResponse('[splatnet3] Non-201 status code', tr, await tr.text());
const error: SplatNet3AuthErrorCode | undefined = AUTH_ERROR_CODES[tr.status as keyof typeof AUTH_ERROR_CODES];
if (error) throw new SplatNet3AuthErrorResponse('[splatnet3] ' + error, tr, await tr.text(), error);
if (tr.status !== 201) throw new SplatNet3ErrorResponse('[splatnet3] Non-201 status code', tr, await tr.text());
const bullet_token = await tr.json() as BulletToken;
const created_at = Date.now();
@ -1083,6 +1093,65 @@ function getMapPersistedQueriesModeFromEnvironment(): MapQueriesMode {
return MapQueriesMode.ALL;
}
export class SplatNet3ErrorResponse<T = unknown> extends ErrorResponse<T> {}
export class SplatNet3AuthErrorResponse extends SplatNet3ErrorResponse {
constructor(
message: string, response: Response | globalThis.Response,
body?: string | unknown | undefined,
readonly code = SplatNet3AuthErrorCode.ERROR_SERVER,
) {
super(message, response, body);
}
}
type GraphQLResponseWithErrors = (GraphQLSuccessResponse & {errors: GraphQLError[]}) | GraphQLErrorResponse;
export class SplatNet3GraphQLErrorResponse<
Id extends string = string,
/** @private */
_Variables extends Id extends KnownRequestId ? VariablesTypes[Id] : unknown =
Id extends KnownRequestId ? VariablesTypes[Id] : unknown,
> extends SplatNet3ErrorResponse<GraphQLResponseWithErrors> {
constructor(
message: string, response: Response | globalThis.Response,
body?: string | GraphQLResponseWithErrors | undefined,
readonly request_id?: Id | string,
readonly variables?: _Variables,
) {
super(message, response, body);
}
static from(response: Response, data: GraphQLResponseWithErrors, id: string, variables: unknown) {
return new SplatNet3GraphQLErrorResponse('[splatnet3] GraphQL error: ' +
data.errors.map(e => e.message).join(', '), response, data, id, variables);
}
}
export class SplatNet3GraphQLResourceNotFoundResponse<
Id extends string = string,
/** @private */
_Result extends Id extends KnownRequestId ? ResultTypes[Id] : unknown =
Id extends KnownRequestId ? ResultTypes[Id] : unknown,
/** @private */
_Variables extends Id extends KnownRequestId ? VariablesTypes[Id] : unknown =
Id extends KnownRequestId ? VariablesTypes[Id] : unknown,
> extends SplatNet3ErrorResponse<PersistedQueryResult<_Result>> {
constructor(
message: string, response: Response | globalThis.Response,
body?: string | PersistedQueryResult<_Result> | undefined,
readonly request_id?: Id | string,
readonly variables?: _Variables,
) {
super(message, response, body);
}
static from(message: string, data: PersistedQueryResult<any>) {
return new SplatNet3GraphQLResourceNotFoundResponse<any, any>(
message, data[ResponseSymbol], data, data[RequestIdSymbol], data[VariablesSymbol]);
}
}
export interface SplatNet3AuthData {
webserviceToken: WebServiceToken;
url: string;

View File

@ -33,8 +33,8 @@ export default class ZncProxyApi implements CoralApiInterface {
debug('fetch %s %s, response %s', method, url, response.status);
if (response.status !== 200 && response.status !== 204) {
throw new ErrorResponse('[zncproxy] Non-200/204 status code', response, await response.text());
if (!response.ok) {
throw new ZncProxyErrorResponse('[zncproxy] Non-2xx status code', response, await response.text());
}
const data = (response.status === 204 ? {} : await response.json()) as T;
@ -182,6 +182,8 @@ function createResult<T extends {}, R>(data: R & {[ResponseSymbol]: Response}, r
return result as Result<T>;
}
export class ZncProxyErrorResponse extends ErrorResponse {}
export interface AuthToken {
user: string;
policy?: AuthPolicy;
@ -227,12 +229,12 @@ export async function getPresenceFromUrl(presence_url: string, useragent?: strin
debug('fetch %s %s, response %s', 'GET', presence_url, response.status);
if (response.status !== 200) {
throw new ErrorResponse('[zncproxy] Unknown error', response, await response.text());
throw new ZncProxyErrorResponse('[zncproxy] Unknown error', response, await response.text());
}
if (!response.headers.get('Content-Type')?.match(/^application\/json(;|$)$/)) {
controller.abort();
throw new ErrorResponse('[zncproxy] Unacceptable content type', response);
throw new ZncProxyErrorResponse('[zncproxy] Unacceptable content type', response);
}
const data = await response.json() as PresenceUrlResponse;

View File

@ -2,7 +2,7 @@ import { dialog, Notification } from './electron.js';
import { App } from './index.js';
import { tryGetNativeImageFromUrl } from './util.js';
import { DiscordPresenceConfiguration, DiscordPresenceExternalMonitorsConfiguration, DiscordPresenceSource } from '../common/types.js';
import { CurrentUser, Friend, Game, CoralErrorResponse } from '../../api/coral-types.js';
import { CurrentUser, Friend, Game, CoralError } from '../../api/coral-types.js';
import { ErrorResponse } from '../../api/util.js';
import { ZncDiscordPresence, ZncProxyDiscordPresence } from '../../common/presence.js';
import { NotificationManager } from '../../common/notify.js';
@ -346,7 +346,7 @@ export class PresenceMonitorManager {
async handleError(
monitor: EmbeddedPresenceMonitor | EmbeddedProxyPresenceMonitor,
err: ErrorResponse<CoralErrorResponse> | NodeJS.ErrnoException
err: ErrorResponse<CoralError> | NodeJS.ErrnoException
): Promise<LoopResult> {
const {response} = await dialog.showMessageBox({
message: err.name + ' updating presence monitor',
@ -366,7 +366,7 @@ export class PresenceMonitorManager {
export class EmbeddedPresenceMonitor extends ZncDiscordPresence {
notifications = new ElectronNotificationManager();
onError?: (error: ErrorResponse<CoralErrorResponse> | NodeJS.ErrnoException) =>
onError?: (error: ErrorResponse<CoralError> | NodeJS.ErrnoException) =>
Promise<LoopResult | void> | LoopResult | void = undefined;
enable() {
@ -409,7 +409,7 @@ export class EmbeddedPresenceMonitor extends ZncDiscordPresence {
}
}
async handleError(err: ErrorResponse<CoralErrorResponse> | NodeJS.ErrnoException): Promise<LoopResult> {
async handleError(err: ErrorResponse<CoralError> | NodeJS.ErrnoException): Promise<LoopResult> {
try {
return await super.handleError(err);
} catch (err: any) {
@ -425,7 +425,7 @@ export class EmbeddedPresenceMonitor extends ZncDiscordPresence {
export class EmbeddedProxyPresenceMonitor extends ZncProxyDiscordPresence {
notifications = new ElectronNotificationManager();
onError?: (error: ErrorResponse<CoralErrorResponse> | NodeJS.ErrnoException) =>
onError?: (error: ErrorResponse<CoralError> | NodeJS.ErrnoException) =>
Promise<LoopResult | void> | LoopResult | void = undefined;
enable() {
@ -468,7 +468,7 @@ export class EmbeddedProxyPresenceMonitor extends ZncProxyDiscordPresence {
}
}
async handleError(err: ErrorResponse<CoralErrorResponse> | NodeJS.ErrnoException): Promise<LoopResult> {
async handleError(err: ErrorResponse<CoralError> | NodeJS.ErrnoException): Promise<LoopResult> {
try {
return await super.handleError(err);
} catch (err: any) {

View File

@ -5,7 +5,7 @@ import { protocol_registration_options } from './index.js';
import { createModalWindow } from './windows.js';
import { tryGetNativeImageFromUrl } from './util.js';
import { WindowType } from '../common/types.js';
import { NintendoAccountAuthError, NintendoAccountSessionAuthorisation, NintendoAccountSessionAuthorisationError, NintendoAccountSessionToken } from '../../api/na.js';
import { NintendoAccountAuthError, NintendoAccountAuthErrorResponse, NintendoAccountSessionAuthorisation, NintendoAccountSessionAuthorisationError, NintendoAccountSessionToken } from '../../api/na.js';
import { NintendoAccountSessionAuthorisationCoral } from '../../api/coral.js';
import { NintendoAccountSessionAuthorisationMoon } from '../../api/moon.js';
import { ErrorResponse } from '../../api/util.js';
@ -329,13 +329,9 @@ export async function addNsoAccount(storage: persist.LocalStorage, use_in_app_br
return {nso, data};
} catch (err) {
if (err instanceof ErrorResponse && err.response.url.startsWith('https://accounts.nintendo.com/')) {
const data: NintendoAccountAuthError = err.data;
if (data.error === 'invalid_grant') {
// The session token has expired/was revoked
return authenticateCoralSessionToken(storage, authenticator, code, true);
}
if (err instanceof NintendoAccountAuthErrorResponse && err.data?.error === 'invalid_grant') {
// The session token has expired/was revoked
return authenticateCoralSessionToken(storage, authenticator, code, true);
}
throw err;
@ -494,13 +490,9 @@ export async function addPctlAccount(storage: persist.LocalStorage, use_in_app_b
return {moon, data};
} catch (err) {
if (err instanceof ErrorResponse && err.response.url.startsWith('https://accounts.nintendo.com/')) {
const data: NintendoAccountAuthError = err.data;
if (data.error === 'invalid_grant') {
// The session token has expired/was revoked
return authenticateMoonSessionToken(storage, authenticator, code, true);
}
if (err instanceof NintendoAccountAuthErrorResponse && err.data?.error === 'invalid_grant') {
// The session token has expired/was revoked
return authenticateMoonSessionToken(storage, authenticator, code, true);
}
throw err;

View File

@ -5,10 +5,9 @@ import express, { Request, RequestHandler, Response } from 'express';
import bodyParser from 'body-parser';
import { v4 as uuidgen } from 'uuid';
import type { Arguments as ParentArguments } from '../nso.js';
import CoralApi, { CoralApiInterface } from '../../api/coral.js';
import CoralApi, { CoralApiInterface, CoralErrorResponse } from '../../api/coral.js';
import { Announcement, CoralStatus, CurrentUser, Friend, FriendCodeUrl, FriendCodeUser, Presence } from '../../api/coral-types.js';
import { AuthPolicy, AuthToken, ZncPresenceEventStreamEvent } from '../../api/znc-proxy.js';
import { ErrorResponse } from '../../api/util.js';
import createDebug from '../../util/debug.js';
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
import { initStorage } from '../../util/storage.js';
@ -685,7 +684,7 @@ class Server extends HttpServer {
const user = await coral.getUserByFriendCode(friendcode);
return [user, id];
} catch (err) {
if (err instanceof ErrorResponse && err.data?.status === CoralStatus.RESOURCE_NOT_FOUND) {
if (err instanceof CoralErrorResponse && err.status === CoralStatus.RESOURCE_NOT_FOUND) {
// A user with this friend code doesn't exist
// This should be cached
return [null, id];

View File

@ -1,6 +1,6 @@
import { Response } from 'node-fetch';
import CoralApi, { CoralApiInterface, CoralAuthData, Result, ZNCA_CLIENT_ID } from '../api/coral.js';
import { Announcements, Friends, Friend, GetActiveEventResult, WebServices, CoralErrorResponse } from '../api/coral-types.js';
import { Announcements, Friends, Friend, GetActiveEventResult, WebServices, CoralError } from '../api/coral-types.js';
import ZncProxyApi from '../api/znc-proxy.js';
import { NintendoAccountSession, Storage } from './storage/index.js';
import { checkUseLimit } from './util.js';
@ -262,7 +262,7 @@ function createTokenExpiredHandler(
session: NintendoAccountSession<SavedToken>, coral: CoralApi,
renew_token_data: {auth_data: SavedToken}, ratelimit = true
) {
return (data: CoralErrorResponse, response: Response) => {
return (data: CoralError, response: Response) => {
debug('Token expired', renew_token_data.auth_data.user.id, data);
return renewToken(session, coral, renew_token_data, ratelimit);
};

View File

@ -3,9 +3,8 @@ import { ConfigureAnalyticsResult, CurrentFestResult, DetailVotingStatusResult,
import createDebug from '../util/debug.js';
import { ZNCA_CLIENT_ID } from '../api/coral.js';
import { NintendoAccountSession, Storage } from './storage/index.js';
import SplatNet3Api, { PersistedQueryResult, SplatNet3AuthData } from '../api/splatnet3.js';
import SplatNet3Api, { PersistedQueryResult, SplatNet3AuthData, SplatNet3AuthErrorCode, SplatNet3AuthErrorResponse } from '../api/splatnet3.js';
import Coral, { SavedToken as SavedCoralToken } from './coral.js';
import { ErrorResponse } from '../api/util.js';
import Users from './users.js';
import { checkUseLimit } from './util.js';
@ -191,7 +190,7 @@ async function renewToken(
debug('Unable to renew bullet token with saved web services token - cached data for this session token doesn\'t exist??');
}
} catch (err) {
if (err instanceof ErrorResponse && err.response.status === 401) {
if (err instanceof SplatNet3AuthErrorResponse && err.code === SplatNet3AuthErrorCode.ERROR_INVALID_GAME_WEB_TOKEN) {
// Web service token invalid/expired...
debug('Web service token expired, authenticating with new token', err);
} else {

View File

@ -1,7 +1,7 @@
import * as persist from 'node-persist';
import { Response } from 'node-fetch';
import CoralApi, { CoralAuthData, ZNCA_CLIENT_ID } from '../../api/coral.js';
import { CoralErrorResponse } from '../../api/coral-types.js';
import { CoralError } from '../../api/coral-types.js';
import ZncProxyApi from '../../api/znc-proxy.js';
import { getNintendoAccountUser, NintendoAccountSessionTokenJwtPayload } from '../../api/na.js';
import createDebug from '../../util/debug.js';
@ -120,7 +120,7 @@ function createTokenExpiredHandler(
storage: persist.LocalStorage, token: string, nso: CoralApi,
renew_token_data: {existingToken: SavedToken}, ratelimit = true
) {
return (data?: CoralErrorResponse, response?: Response) => {
return (data?: CoralError, response?: Response) => {
debug('Token expired', renew_token_data.existingToken.user.id, data);
return renewToken(storage, token, nso, renew_token_data, ratelimit);
};

View File

@ -1,12 +1,11 @@
import persist from 'node-persist';
import { Response } from 'node-fetch';
import { getToken, Login, SavedToken } from './coral.js';
import SplatNet3Api, { SplatNet3AuthData } from '../../api/splatnet3.js';
import SplatNet3Api, { SplatNet3AuthData, SplatNet3AuthErrorCode, SplatNet3AuthErrorResponse } from '../../api/splatnet3.js';
import { checkUseLimit, SHOULD_LIMIT_USE } from './util.js';
import createDebug from '../../util/debug.js';
import { Jwt } from '../../util/jwt.js';
import { NintendoAccountSessionTokenJwtPayload } from '../../api/na.js';
import { ErrorResponse } from '../../api/util.js';
const debug = createDebug('nxapi:auth:splatnet3');
@ -121,7 +120,7 @@ async function renewToken(
debug('Unable to renew bullet token with saved web services token - cached data for this session token doesn\'t exist??');
}
} catch (err) {
if (err instanceof ErrorResponse && err.response.status === 401) {
if (err instanceof SplatNet3AuthErrorResponse && err.code === SplatNet3AuthErrorCode.ERROR_INVALID_GAME_WEB_TOKEN) {
// Web service token invalid/expired...
debug('Web service token expired, authenticating with new token', err);
} else {

View File

@ -1,6 +1,6 @@
import persist from 'node-persist';
import { CoralApiInterface } from '../api/coral.js';
import { ActiveEvent, Announcements, CurrentUser, Friend, Game, Presence, PresenceState, WebServices, CoralErrorResponse, GetActiveEventResult } from '../api/coral-types.js';
import { ActiveEvent, Announcements, CurrentUser, Friend, Game, Presence, PresenceState, WebServices, CoralError, GetActiveEventResult } from '../api/coral-types.js';
import ZncProxyApi from '../api/znc-proxy.js';
import { ErrorResponse } from '../api/util.js';
import { SavedToken } from './auth/coral.js';
@ -179,7 +179,7 @@ export class ZncNotifications extends Loop {
if (user) await this.updatePresenceForSplatNet2Monitors([user]);
}
async handleError(err: ErrorResponse<CoralErrorResponse> | NodeJS.ErrnoException): Promise<LoopResult> {
async handleError(err: ErrorResponse<CoralError> | NodeJS.ErrnoException): Promise<LoopResult> {
return handleError(err, this);
}
}

View File

@ -3,7 +3,7 @@ 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, ZncNotifications } from './notify.js';
import { ActiveEvent, CurrentUser, Friend, Game, Presence, PresenceState, CoralErrorResponse } from '../api/coral-types.js';
import { ActiveEvent, CurrentUser, Friend, Game, Presence, PresenceState, CoralError } from '../api/coral-types.js';
import { getPresenceFromUrl } from '../api/znc-proxy.js';
import createDebug from '../util/debug.js';
import { ErrorResponse, ResponseSymbol } from '../api/util.js';
@ -499,7 +499,7 @@ export class ZncDiscordPresence extends ZncNotifications {
this.discord.title = {id: title_id, since: saved_presence.title_since};
}
async handleError(err: ErrorResponse<CoralErrorResponse> | NodeJS.ErrnoException): Promise<LoopResult> {
async handleError(err: ErrorResponse<CoralError> | NodeJS.ErrnoException): Promise<LoopResult> {
this.discord.onError(err);
return super.handleError(err);
@ -759,7 +759,7 @@ export class ZncProxyDiscordPresence extends Loop {
}
}
async handleError(err: ErrorResponse<CoralErrorResponse> | NodeJS.ErrnoException): Promise<LoopResult> {
async handleError(err: ErrorResponse<CoralError> | NodeJS.ErrnoException): Promise<LoopResult> {
this.discord.onError(err);
return handleError(err, this);

View File

@ -2,12 +2,11 @@ import * as path from 'node:path';
import * as fs from 'node:fs/promises';
import persist from 'node-persist';
import mkdirp from 'mkdirp';
import SplatNet2Api from '../../api/splatnet2.js';
import SplatNet2Api, { SplatNet2ErrorResponse } from '../../api/splatnet2.js';
import { renewIksmToken } from '../auth/splatnet2.js';
import { Records, Stages, WebServiceError } from '../../api/splatnet2-types.js';
import { Records, Stages } from '../../api/splatnet2-types.js';
import { dumpCoopResults, dumpResults } from './dump-results.js';
import { dumpProfileImage, dumpRecords } from './dump-records.js';
import { ErrorResponse } from '../../api/util.js';
import createDebug from '../../util/debug.js';
import Loop, { LoopResult } from '../../util/loop.js';
@ -90,8 +89,8 @@ export class SplatNet2RecordsMonitor extends Loop {
}
}
async handleError(err: Error | ErrorResponse<WebServiceError>): Promise<LoopResult> {
if ('response' in err && err.data?.code === 'AUTHENTICATION_ERROR') {
async handleError(err: Error): Promise<LoopResult> {
if (err instanceof SplatNet2ErrorResponse && err.data?.code === 'AUTHENTICATION_ERROR') {
// Token expired
debug('Renewing iksm_session cookie');

View File

@ -8,6 +8,8 @@ export {
ResponseDataSymbol,
CorrelationIdSymbol,
CoralErrorResponse,
NintendoAccountSessionAuthorisationCoral,
} from '../api/coral.js';

View File

@ -3,6 +3,8 @@ export {
MoonAuthData,
PartialMoonAuthData,
MoonErrorResponse,
NintendoAccountSessionAuthorisationMoon,
} from '../api/moon.js';

View File

@ -2,6 +2,9 @@ export {
NintendoAccountSessionAuthorisation,
NintendoAccountSessionAuthorisationError,
NintendoAccountAuthErrorResponse,
NintendoAccountErrorResponse,
NintendoAccountSessionToken,
NintendoAccountToken,
NintendoAccountAuthError,

View File

@ -2,6 +2,8 @@ export {
default as NooklinkApi,
NooklinkAuthData,
NooklinkErrorResponse,
NooklinkUserApi,
NooklinkUserAuthData,
PartialNooklinkUserAuthData,

View File

@ -3,6 +3,8 @@ export {
SplatNet2AuthData,
SplatNet2CliTokenData,
SplatNet2ErrorResponse,
LeagueType,
LeagueRegion,
ShareColour as ShareProfileColour,

View File

@ -6,6 +6,11 @@ export {
RequestIdSymbol,
VariablesSymbol,
SplatNet3ErrorResponse,
SplatNet3AuthErrorResponse,
SplatNet3GraphQLErrorResponse,
SplatNet3GraphQLResourceNotFoundResponse,
XRankingRegion,
XRankingLeaderboardType,
XRankingLeaderboardRule,

View File

@ -2,7 +2,6 @@ import { AbortError } from 'node-fetch';
import createDebug from './debug.js';
import Loop, { LoopResult } from './loop.js';
import { TemporaryErrorSymbol } from './misc.js';
import { CoralErrorResponse } from '../api/coral-types.js';
import { ErrorResponse } from '../api/util.js';
const debug = createDebug('nxapi:util:errors');
@ -28,7 +27,7 @@ export const temporary_http_errors = [
];
export async function handleError(
err: ErrorResponse<CoralErrorResponse> | NodeJS.ErrnoException,
err: ErrorResponse | NodeJS.ErrnoException,
loop: Loop,
): Promise<LoopResult> {
if (TemporaryErrorSymbol in err && err[TemporaryErrorSymbol]) {