Switch to undici

This commit is contained in:
Samuel Elliott
2023-07-12 23:15:37 +01:00
parent 10a6784fa7
commit d7a64b9807
32 changed files with 155 additions and 75 deletions

View File

@@ -1,4 +1,4 @@
import fetch, { Response } from 'node-fetch';
import { fetch, Response } from 'undici';
import { v4 as uuidgen } from 'uuid';
import createDebug from '../util/debug.js';
import { JwtPayload } from '../util/jwt.js';
@@ -145,7 +145,7 @@ export default class CoralApi implements CoralApiInterface {
debug('fetch %s %s, response %s', method, url, response.status);
if (response.status !== 200) {
throw new CoralErrorResponse('[znc] Non-200 status code', response, await response.text());
throw await CoralErrorResponse.fromResponse(response, '[znc] Non-200 status code');
}
const data = await response.json() as CoralResponse<T>;
@@ -449,7 +449,7 @@ export default class CoralApi implements CoralApiInterface {
debug('fetch %s %s, response %s', 'POST', '/v3/Account/Login', response.status);
if (response.status !== 200) {
throw new CoralErrorResponse('[znc] Non-200 status code', response, await response.text());
throw await CoralErrorResponse.fromResponse(response, '[znc] Non-200 status code');
}
const data = await response.json() as CoralResponse<AccountLogin>;

View File

@@ -1,5 +1,5 @@
import process from 'node:process';
import fetch, { Headers } from 'node-fetch';
import { fetch, Headers } from 'undici';
import { v4 as uuidgen } from 'uuid';
import { defineResponse, ErrorResponse } from './util.js';
import createDebug from '../util/debug.js';
@@ -61,7 +61,7 @@ export async function flapg(
}).finally(cancel);
if (response.status !== 200) {
throw new ErrorResponse<FlapgApiError>('[flapg] Non-200 status code', response, await response.text());
throw await ErrorResponse.fromResponse(response, '[flapg] Non-200 status code');
}
const data = await response.json() as FlapgApiResponse;
@@ -142,13 +142,13 @@ export async function iminkf(
}).finally(cancel);
if (response.status !== 200) {
throw new ErrorResponse<IminkFError>('[imink] Non-200 status code', response, await response.text());
throw await ErrorResponse.fromResponse(response, '[imink] Non-200 status code');
}
const data = await response.json() as IminkFResponse | IminkFError;
if ('error' in data) {
throw new ErrorResponse<IminkFError>('[imink] ' + data.reason, response, data);
throw new ErrorResponse('[imink] ' + data.reason, response, data);
}
debugImink('Got f parameter "%s"', data.f);
@@ -229,7 +229,7 @@ export async function genf(
}).finally(cancel);
if (response.status !== 200) {
throw new ErrorResponse<AndroidZncaFError>('[znca-api] Non-200 status code', response, await response.text());
throw await ErrorResponse.fromResponse(response, '[znca-api] Non-200 status code');
}
const data = await response.json() as AndroidZncaFResponse | AndroidZncaFError;

View File

@@ -1,4 +1,4 @@
import fetch, { Response } from 'node-fetch';
import { fetch, Response } from 'undici';
import { generateAuthData, getNintendoAccountToken, getNintendoAccountUser, NintendoAccountSessionAuthorisation, NintendoAccountToken, NintendoAccountUser } from './na.js';
import { defineResponse, ErrorResponse, HasResponse } from './util.js';
import { DailySummaries, Devices, MonthlySummaries, MonthlySummary, MoonError, ParentalControlSettingState, SmartDevices, User } from './moon-types.js';
@@ -86,7 +86,7 @@ export default class MoonApi {
}
if (response.status !== 200) {
throw new MoonErrorResponse('[moon] Non-200 status code', response, await response.text());
throw await MoonErrorResponse.fromResponse(response, '[moon] Non-200 status code');
}
const data = await response.json() as T | MoonError;

View File

@@ -1,5 +1,5 @@
import * as crypto from 'node:crypto';
import fetch, { Response } from 'node-fetch';
import { fetch, Response } from 'undici';
import { defineResponse, ErrorResponse, HasResponse } from './util.js';
import createDebug from '../util/debug.js';
import { JwtPayload } from '../util/jwt.js';
@@ -127,7 +127,7 @@ export async function getNintendoAccountSessionToken(code: string, verifier: str
}).finally(cancel);
if (response.status !== 200) {
throw new NintendoAccountAuthErrorResponse('[na] Non-200 status code', response, await response.text());
throw await NintendoAccountAuthErrorResponse.fromResponse(response, '[na] Non-200 status code');
}
const token = await response.json() as NintendoAccountSessionToken | NintendoAccountAuthError | NintendoAccountError;
@@ -164,7 +164,7 @@ export async function getNintendoAccountToken(token: string, client_id: string)
}).finally(cancel);
if (response.status !== 200) {
throw new NintendoAccountAuthErrorResponse('[na] Non-200 status code', response, await response.text());
throw await NintendoAccountAuthErrorResponse.fromResponse(response, '[na] Non-200 status code');
}
const nintendoAccountToken = await response.json() as NintendoAccountToken | NintendoAccountAuthError | NintendoAccountError;
@@ -198,7 +198,7 @@ export async function getNintendoAccountUser(token: NintendoAccountToken) {
}).finally(cancel);
if (response.status !== 200) {
throw new NintendoAccountErrorResponse('[na] Non-200 status code', response, await response.text());
throw await NintendoAccountErrorResponse.fromResponse(response, '[na] Non-200 status code');
}
const user = await response.json() as NintendoAccountUser | NintendoAccountError;

View File

@@ -1,4 +1,4 @@
import fetch, { Response } from 'node-fetch';
import { fetch, FormData, Response } from 'undici';
import { WebServiceToken } from './coral-types.js';
import { NintendoAccountUser } from './na.js';
import { defineResponse, ErrorResponse, HasResponse } from './util.js';
@@ -81,7 +81,7 @@ export default class NooklinkApi {
}
if (!response.ok) {
throw new NooklinkErrorResponse('[nooklink] Non-2xx status code', response, await response.text());
throw await NooklinkErrorResponse.fromResponse(response, '[nooklink] Non-2xx status code');
}
const data = await response.json() as T | WebServiceError;
@@ -172,12 +172,12 @@ export default class NooklinkApi {
debug('fetch %s %s, response %s', 'GET', url, response.status);
const body = await response.text();
if (response.status !== 200) {
throw new NooklinkErrorResponse('[nooklink] Non-200 status code', response, body);
throw await NooklinkErrorResponse.fromResponse(response, '[nooklink] Non-200 status code');
}
const body = await response.text();
const cookies = response.headers.get('Set-Cookie');
const match = cookies?.match(/\b_gtoken=([^;]*)(;(\s*((?!expires)[a-z]+=([^;]*));?)*(\s*(expires=([^;]*));?)?|$)/i);

View File

@@ -1,4 +1,4 @@
import fetch from 'node-fetch';
import { Cookie, fetch, FormData, getSetCookies } from 'undici';
import { v4 as uuidgen } from 'uuid';
import { WebServiceToken } from './coral-types.js';
import { NintendoAccountUser } from './na.js';
@@ -63,7 +63,7 @@ export default class SplatNet2Api {
}
if (response.status !== 200) {
throw new SplatNet2ErrorResponse('[splatnet2] Non-200 status code', response, await response.text());
throw await SplatNet2ErrorResponse.fromResponse(response, '[splatnet2] Non-200 status code');
}
updateIksmSessionLastUsed.handler?.call(null, this.iksm_session);
@@ -302,27 +302,22 @@ ${colour}
debug('fetch %s %s, response %s', 'GET', url, response.status);
const body = await response.text();
if (response.status !== 200) {
throw new SplatNet2ErrorResponse('[splatnet2] Non-200 status code', response, body);
throw await SplatNet2ErrorResponse.fromResponse(response, '[splatnet2] Non-200 status code');
}
const cookies = response.headers.get('Set-Cookie');
const match = cookies?.match(/\biksm_session=([^;]*)(;(\s*((?!expires)[a-z]+=([^;]*));?)*(\s*(expires=([^;]*));?)?|$)/i);
const body = await response.text();
if (!match) {
const cookies = getSetCookies(response.headers);
const iksm_session = cookies.find(c => c.name === 'iksm_session');
if (!iksm_session) {
throw new SplatNet2ErrorResponse('[splatnet2] Response didn\'t include iksm_session cookie', response, body);
}
const iksm_session = decodeURIComponent(match[1]);
// Nintendo sets the expires field to an invalid timestamp - browsers don't care but Data.parse does
const expires = decodeURIComponent(match[8] || '')
.replace(/(\b)(\d{1,2})-([a-z]{3})-(\d{4})(\b)/gi, '$1$2 $3 $4$5');
const expires_at: number = (iksm_session.expires as Date)?.getTime() ?? Date.now() + 24 * 60 * 60 * 1000;
debug('iksm_session %s, expires %s', iksm_session.replace(/^(.{6}).*/, '$1****'), expires);
const expires_at = expires ? Date.parse(expires) : Date.now() + 24 * 60 * 60 * 1000;
debug('iksm_session %s, expires %s', iksm_session.value.replace(/^(.{6}).*/, '$1****'), iksm_session.expires);
const ml = body.match(/<html(?:\s+[a-z0-9-]+(?:=(?:"[^"]*"|[^\s>]*))?)*\s+lang=(?:"([^"]*)"|([^\s>]*))/i);
const mr = body.match(/<html(?:\s+[a-z0-9-]+(?:=(?:"[^"]*"|[^\s>]*))?)*\s+data-region=(?:"([^"]*)"|([^\s>]*))/i);
@@ -345,14 +340,14 @@ ${colour}
return {
webserviceToken,
url: url.toString(),
cookies: cookies!,
cookies,
body,
language,
region,
user_id,
nsa_id,
iksm_session,
iksm_session: iksm_session.value,
expires_at,
useragent: SPLATNET2_WEBSERVICE_USERAGENT,
};
@@ -364,7 +359,7 @@ export class SplatNet2ErrorResponse extends ErrorResponse<WebServiceError> {}
export interface SplatNet2AuthData {
webserviceToken: WebServiceToken;
url: string;
cookies: string;
cookies: string | Cookie[];
body: string;
language: string;

View File

@@ -1,4 +1,4 @@
import fetch, { Response } from 'node-fetch';
import { fetch, Response } from 'undici';
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';
@@ -102,7 +102,7 @@ export default class SplatNet3Api {
) {}
async fetch<T = unknown>(
url: string, method = 'GET', body?: string | FormData, headers?: object,
url: string, method = 'GET', body?: string, headers?: object,
/** @internal */ _log?: string,
/** @internal */ _attempt = 0,
): Promise<HasResponse<T, Response>> {
@@ -153,7 +153,7 @@ export default class SplatNet3Api {
}
if (response.status !== 200) {
throw new SplatNet3ErrorResponse('[splatnet3] Non-200 status code', response, await response.text());
throw await SplatNet3ErrorResponse.fromResponse(response, '[splatnet3] Non-200 status code');
}
const remaining = parseInt(response.headers.get('x-bullettoken-remaining') ?? '0');
@@ -1029,12 +1029,12 @@ export default class SplatNet3Api {
debug('fetch %s %s, response %s', 'GET', url, response.status);
const body = await response.text();
if (response.status !== 200) {
throw new SplatNet3ErrorResponse('[splatnet3] Non-200 status code', response, body);
throw await SplatNet3ErrorResponse.fromResponse(response, '[splatnet3] Non-200 status code');
}
const body = await response.text();
const cookies = response.headers.get('Set-Cookie');
const [signal2, cancel2] = timeoutSignal();
@@ -1058,8 +1058,8 @@ export default class SplatNet3Api {
debug('fetch %s %s, response %s', 'POST', '/bullet_tokens', response.status);
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());
if (error) throw await SplatNet3AuthErrorResponse.fromResponse(tr, '[splatnet3] ' + error);
if (tr.status !== 201) throw await SplatNet3ErrorResponse.fromResponse(tr, '[splatnet3] Non-201 status code');
const bullet_token = await tr.json() as BulletToken;
const created_at = Date.now();
@@ -1099,7 +1099,8 @@ export class SplatNet3AuthErrorResponse extends SplatNet3ErrorResponse {
constructor(
message: string, response: Response | globalThis.Response,
body?: string | unknown | undefined,
readonly code = SplatNet3AuthErrorCode.ERROR_SERVER,
readonly code = AUTH_ERROR_CODES[response.status as keyof typeof AUTH_ERROR_CODES] ??
SplatNet3AuthErrorCode.ERROR_SERVER,
) {
super(message, response, body);
}

View File

@@ -1,5 +1,6 @@
import * as util from 'node:util';
import { Response as NodeFetchResponse } from 'node-fetch';
import { Response as UndiciResponse } from 'undici';
export const ResponseSymbol = Symbol('Response');
const ErrorResponseSymbol = Symbol('IsErrorResponse');
@@ -20,13 +21,17 @@ export class ErrorResponse<T = unknown> extends Error {
constructor(
message: string,
readonly response: Response | NodeFetchResponse,
body?: string | T
readonly response: Response | NodeFetchResponse | UndiciResponse,
body?: string | ArrayBuffer | T
) {
super(message);
Object.defineProperty(this, ErrorResponseSymbol, {enumerable: false, value: ErrorResponseSymbol});
if (body instanceof ArrayBuffer) {
body = (new TextDecoder()).decode(body);
}
if (typeof body === 'string') {
this.body = body;
try {
@@ -50,6 +55,12 @@ export class ErrorResponse<T = unknown> extends Error {
(lines.length ? '\n' + lines.join('\n') : ''),
});
}
static async fromResponse(response: UndiciResponse, message: string) {
const body = await response.arrayBuffer();
return new this(message, response, body);
}
}
Object.defineProperty(ErrorResponse, Symbol.hasInstance, {

View File

@@ -1,4 +1,4 @@
import fetch, { Response } from 'node-fetch';
import { fetch, Response } from 'undici';
import { ActiveEvent, Announcements, CurrentUser, Event, Friend, Presence, PresencePermissions, User, WebService, WebServiceToken, CoralStatus, CoralSuccessResponse, FriendCodeUser, FriendCodeUrl } from './coral-types.js';
import { defineResponse, ErrorResponse, ResponseSymbol } from './util.js';
import { CoralApiInterface, CoralAuthData, CorrelationIdSymbol, PartialCoralAuthData, ResponseDataSymbol, Result } from './coral.js';
@@ -34,7 +34,7 @@ export default class ZncProxyApi implements CoralApiInterface {
debug('fetch %s %s, response %s', method, url, response.status);
if (!response.ok) {
throw new ZncProxyErrorResponse('[zncproxy] Non-2xx status code', response, await response.text());
throw await ZncProxyErrorResponse.fromResponse(response, '[zncproxy] Non-2xx status code');
}
const data = (response.status === 204 ? {} : await response.json()) as T;
@@ -229,11 +229,11 @@ 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 ZncProxyErrorResponse('[zncproxy] Unknown error', response, await response.text());
throw await ZncProxyErrorResponse.fromResponse(response, '[zncproxy] Non-200 status code');
}
if (!response.headers.get('Content-Type')?.match(/^application\/json(;|$)$/)) {
controller.abort();
response.body?.cancel();
throw new ZncProxyErrorResponse('[zncproxy] Unacceptable content type', response);
}