Skip to content

Commit

Permalink
Merge pull request #65 from GW2Treasures/feature/on-request
Browse files Browse the repository at this point in the history
Add `onRequest` option
  • Loading branch information
darthmaim authored Jul 26, 2024
2 parents 27a83df + 0f5f1be commit 3f5f34b
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 30 deletions.
5 changes: 5 additions & 0 deletions .changeset/new-apes-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@gw2api/fetch": minor
---

Add `onRequest` option to modify requests
84 changes: 54 additions & 30 deletions packages/fetch/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ type Args<Url extends string, Schema extends SchemaVersion> = RequiredKeys<Optio
? [endpoint: Url, options?: FetchGw2ApiOptions<Schema> & OptionsByEndpoint<Url> & FetchOptions]
: [endpoint: Url, options: FetchGw2ApiOptions<Schema> & OptionsByEndpoint<Url> & FetchOptions]

export function fetchGw2Api<
export async function fetchGw2Api<
Url extends KnownEndpoint | (string & {}),
Schema extends SchemaVersion = undefined
>(
Expand All @@ -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<Schema extends SchemaVersion> = {
/** 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<Request>;

/**
* onResponse handler. Called for all responses, successful or not.
* Make sure to clone the response in case of consuming the body.
Expand Down

0 comments on commit 3f5f34b

Please sign in to comment.