@gw2api/fetch
Version:
Tiny wrapper around fetch that returns type-safe responses
88 lines (87 loc) • 4.35 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
export function fetchGw2Api() {
return __awaiter(this, arguments, void 0, function* (...[endpoint, options]) {
var _a;
const url = new URL(endpoint, 'https://api.guildwars2.com/');
if (options.schema) {
url.searchParams.set('v', options.schema);
}
if (hasLanguage(options)) {
url.searchParams.set('lang', options.language);
}
if (hasAccessToken(options)) {
url.searchParams.set('access_token', options.accessToken);
}
// build request
let request = new Request(url, {
// The GW2 API never uses redirects, so we want to error if we encounter one.
// We use `manual` instead of `error` here so we can throw our own `Gw2ApiError` with the response attached
redirect: 'manual',
// set signal and cache from options
signal: options.signal,
cache: options.cache
});
// if there is a onRequest handler registered, let it modify the request
if (options.onRequest) {
request = yield options.onRequest(request);
if (!(request instanceof Request)) {
throw new Error(`onRequest has to return a Request`);
}
}
// call the API
const response = yield fetch(request);
// call onResponse handler
yield ((_a = options.onResponse) === null || _a === void 0 ? void 0 : _a.call(options, response));
// check if the response is json (`application/json; charset=utf-8`)
const isJson = response.headers.get('content-type').startsWith('application/json');
// censor access token in url to not leak it in error messages
const erroredUrl = hasAccessToken(options)
? url.toString().replace(options.accessToken, '***')
: url.toString();
// check if the response is an error
if (!response.ok) {
// if the response is JSON, it might have more details in the `text` prop
if (isJson) {
const error = yield response.json();
if (typeof error === 'object' && 'text' in error && typeof error.text === 'string') {
throw new Gw2ApiError(`The GW2 API call to '${erroredUrl}' returned ${response.status} ${response.statusText}: ${error.text}.`, response);
}
}
// otherwise just throw error with the status code
throw new Gw2ApiError(`The GW2 API call to '${erroredUrl}' returned ${response.status} ${response.statusText}.`, response);
}
// if the response is not JSON, throw an error
if (!isJson) {
throw new Gw2ApiError(`The GW2 API call to '${erroredUrl}' did not respond with a JSON response`, response);
}
// parse json
const json = yield response.json();
// check that json is not `["v1", "v2"]` which sometimes happens for authenticated endpoints
if (url.toString() !== 'https://api.guildwars2.com/' && Array.isArray(json) && json.length === 2 && json[0] === 'v1' && json[1] === 'v2') {
throw new Gw2ApiError(`The GW2 API call to '${erroredUrl}' did returned an invalid response (["v1", "v2"])`, response);
}
// TODO: catch more errors
return json;
});
}
export class Gw2ApiError extends Error {
constructor(message, response) {
super(message);
this.response = response;
this.name = 'Gw2ApiError';
}
}
function hasLanguage(options) {
return 'language' in options;
}
function hasAccessToken(options) {
return 'accessToken' in options;
}