Skip to content

Commit

Permalink
Improving SDK error/response handling (directus#19539)
Browse files Browse the repository at this point in the history
* improving error/response handling

* updated request function to expose the response on error

* updated the composables where needed

* Create few-rules-talk.md

* ran prettier

* undid unintended type change

* added missing awaits for onResponse

* Update few-rules-talk.md

* Mark it as major change

* unpack directus errors instead of nesting

* ran prettier

* Update .changeset/few-rules-talk.md

Co-authored-by: Pascal Jufer <pascal-jufer@bluewin.ch>

---------

Co-authored-by: Pascal Jufer <pascal-jufer@bluewin.ch>
  • Loading branch information
2 people authored and br-rafaelbarros committed Nov 7, 2023
1 parent e2e6747 commit b2cfcdc
Show file tree
Hide file tree
Showing 6 changed files with 50 additions and 56 deletions.
5 changes: 5 additions & 0 deletions .changeset/few-rules-talk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@directus/sdk": minor
---

Included the response object in thrown errors and added the request object in the `onResponse` hook
6 changes: 2 additions & 4 deletions sdk/src/auth/composable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,7 @@ export const authentication = (mode: AuthenticationMode = 'cookie', config: Auth

const requestUrl = getRequestUrl(client.url, '/auth/refresh');

const data = await request<AuthenticationData>(requestUrl.toString(), options).catch((err) => {
throw err;
});
const data = await request<AuthenticationData>(requestUrl.toString(), options);

setCredentials(data);
return data;
Expand Down Expand Up @@ -155,7 +153,7 @@ export const authentication = (mode: AuthenticationMode = 'cookie', config: Auth
}

const requestUrl = getRequestUrl(client.url, '/auth/logout');
await request(requestUrl.toString(), options, null);
await request(requestUrl.toString(), options);

if (refreshTimeout) clearTimeout(refreshTimeout);
resetStorage();
Expand Down
22 changes: 9 additions & 13 deletions sdk/src/rest/composable.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { StaticTokenClient } from '../auth/types.js';
import type { DirectusClient } from '../types/client.js';
import type { ResponseTransformer } from '../index.js';
import { getRequestUrl } from '../utils/get-request-url.js';
import { request } from '../utils/request.js';
import type { RestClient, RestCommand, RestConfig } from './types.js';
Expand Down Expand Up @@ -57,22 +56,19 @@ export const rest = (config: RestConfig = {}) => {
fetchOptions = await config.onRequest(fetchOptions);
}

//const onError = config.onError ?? ((_err: any) => undefined);
let onResponse: ResponseTransformer | undefined | null;
let result = await request<Output>(requestUrl.toString(), fetchOptions);

// chain response handlers if needed
if (config.onResponse && options.onResponse) {
onResponse = ((data: any) =>
Promise.resolve(data).then(config.onResponse).then(options.onResponse)) as ResponseTransformer;
} else if ('onResponse' in options) {
onResponse = options.onResponse;
} else if ('onResponse' in config) {
onResponse = config.onResponse;
// apply onResponse hook from command
if ('onResponse' in options) {
result = await options.onResponse(result, fetchOptions);
}

const response = await request(requestUrl.toString(), fetchOptions, onResponse); //.catch(onError);
// apply global onResponse hook
if ('onResponse' in config) {
result = await config.onResponse(result, fetchOptions);
}

return response as Output;
return result as Output;
},
};
};
Expand Down
2 changes: 1 addition & 1 deletion sdk/src/types/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ export interface RequestTransformer {
}

export interface ResponseTransformer {
<Output = any>(data: any): Output | Promise<Output>;
<Output = any>(data: any, request: RequestInit): Output | Promise<Output>;
}
24 changes: 24 additions & 0 deletions sdk/src/utils/extract-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
*
* @param {Response} response
* @returns {any}
*/
export async function extractData(response: Response) {
const type = response.headers.get('Content-Type')?.toLowerCase();

if (type?.startsWith('application/json')) {
const result = await response.json();
if (!response.ok) throw result;
if ('data' in result) return result.data;
return result;
}

if (type?.startsWith('text/html') || type?.startsWith('text/plain')) {
const result = await response.text();
if (!response.ok) throw result;
return result;
}

// empty body fallback
return;
}
47 changes: 9 additions & 38 deletions sdk/src/utils/request.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { extractData } from './extract-data.js';

/**
* Request helper providing default settings
*
Expand All @@ -6,49 +8,18 @@
*
* @returns The API result if successful
*/
export const request = async <Output = any>(
url: string,
options: RequestInit,
formatter?: ((data: any) => Output) | null
): Promise<Output> => {
export const request = async <Output = any>(url: string, options: RequestInit): Promise<Output> => {
options.headers =
typeof options.headers === 'object' && !Array.isArray(options.headers)
? (options.headers as Record<string, string>)
: {};

const defaultFormatter = (data: Output | { data: Output }) => {
if (typeof data === 'object' && data && 'data' in data) {
return data.data;
}

return data;
};

const outputFormatter = formatter !== undefined && formatter !== null ? formatter : defaultFormatter;

const response = await globalThis
.fetch(url, options)
.then(async (response) => {
const type = response.headers.get('Content-Type')?.toLowerCase();

if (type?.startsWith('application/json')) {
const result = await response.json();
if (!response.ok) throw result;
return result;
}

if (type?.startsWith('text/html') || type?.startsWith('text/plain')) {
const result = await response.text();
if (!response.ok) throw result;
return result;
}
const response = await globalThis.fetch(url, options);

// empty body fallback
return;
})
.catch((err) => {
throw err;
});
const data = await extractData(response).catch((reason) => {
const errors = typeof reason === 'object' && 'errors' in reason ? reason.errors : reason;
throw { errors, response };
});

return outputFormatter(response);
return data;
};

0 comments on commit b2cfcdc

Please sign in to comment.