diff --git a/.changeset/new-apes-accept.md b/.changeset/new-apes-accept.md new file mode 100644 index 0000000..6ce39da --- /dev/null +++ b/.changeset/new-apes-accept.md @@ -0,0 +1,5 @@ +--- +"@gw2api/fetch": minor +--- + +Add `onRequest` option to modify requests diff --git a/packages/fetch/src/index.ts b/packages/fetch/src/index.ts index ffbc133..70b6b92 100644 --- a/packages/fetch/src/index.ts +++ b/packages/fetch/src/index.ts @@ -8,7 +8,7 @@ type Args = RequiredKeys & OptionsByEndpoint & FetchOptions] : [endpoint: Url, options: FetchGw2ApiOptions & OptionsByEndpoint & FetchOptions] -export function fetchGw2Api< +export async function fetchGw2Api< Url extends KnownEndpoint | (string & {}), Schema extends SchemaVersion = undefined >( @@ -26,51 +26,75 @@ export function fetchGw2Api< url.searchParams.set('access_token', options.accessToken); } - return fetch(url, { redirect: 'manual', signal: options.signal, cache: options.cache }).then(async (r) => { - // call onResponse handler - await options.onResponse?.(r); + // 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', - // check if the response is json (`application/json; charset=utf-8`) - const isJson = r.headers.get('content-type').startsWith('application/json'); - - // check if the response is an error - if(!r.ok) { - // if the response is JSON, it might have more details in the `text` prop - if(isJson) { - const error: unknown = await r.json(); + // set signal and cache from options + signal: options.signal, + cache: options.cache + }); - if(typeof error === 'object' && 'text' in error && typeof error.text === 'string') { - throw new Gw2ApiError(`The GW2 API call to '${url.toString()}' returned ${r.status} ${r.statusText}: ${error.text}.`, r); - } - } + // if there is a onRequest handler registered, let it modify the request + if(options.onRequest) { + request = await options.onRequest(request); - // otherwise just throw error with the status code - throw new Gw2ApiError(`The GW2 API call to '${url.toString()}' returned ${r.status} ${r.statusText}.`, r); + if(!(request instanceof Request)) { + throw new Error(`onRequest has to return a Request`); } + } - // if the response is not JSON, throw an error - if(!isJson) { - throw new Gw2ApiError(`The GW2 API call to '${url.toString()}' did not respond with a JSON response`, r); - } + // call the API + const response = await fetch(request); - // parse json - const json = await r.json(); + // call onResponse handler + await options.onResponse?.(response); - // 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 '${url.toString()}' did returned an invalid response (["v1", "v2"])`, r); + // check if the response is json (`application/json; charset=utf-8`) + const isJson = response.headers.get('content-type').startsWith('application/json'); + + // 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: unknown = await response.json(); + + if(typeof error === 'object' && 'text' in error && typeof error.text === 'string') { + throw new Gw2ApiError(`The GW2 API call to '${url.toString()}' returned ${response.status} ${response.statusText}: ${error.text}.`, response); + } } - // TODO: catch more errors + // otherwise just throw error with the status code + throw new Gw2ApiError(`The GW2 API call to '${url.toString()}' returned ${response.status} ${response.statusText}.`, response); + } - return json; - }); + // if the response is not JSON, throw an error + if(!isJson) { + throw new Gw2ApiError(`The GW2 API call to '${url.toString()}' did not respond with a JSON response`, response); + } + + // parse json + const json = await 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 '${url.toString()}' did returned an invalid response (["v1", "v2"])`, response); + } + + // TODO: catch more errors + + return json; } export type FetchGw2ApiOptions = { /** The schema to use when making the API request */ schema?: Schema; + /** onRequest handler allows to modify the request made to the Guild Wars 2 API. */ + onRequest?: (request: Request) => Request | Promise; + /** * onResponse handler. Called for all responses, successful or not. * Make sure to clone the response in case of consuming the body.