Update fetcher and add good coverage to it

This commit is contained in:
Pavel Mineev
2021-02-02 15:53:40 -06:00
committed by Umputun
parent 77825ce947
commit 2fae0c29d2
6 changed files with 338 additions and 315 deletions
+14
View File
@@ -1,6 +1,20 @@
global.Headers = class HeadersMock extends Headers implements Headers {
private headers = new Map();
constructor(headers?: HeadersInit) {
super(headers);
if (typeof headers === 'object' && headers !== null) {
const entries = Object.entries(headers) as [string, string][];
if (entries.length > 0) {
entries.forEach(([k, v]) => {
this.append(k, v);
});
}
}
}
append(key: string, value: string) {
this.headers.set(key, value);
}
+70 -152
View File
@@ -3,18 +3,39 @@ import { BASE_URL, API_BASE } from './constants';
import { Config, Comment, Tree, User, BlockedUser, Sorting, AuthProvider, BlockTTL, Image } from './types';
import fetcher from './fetcher';
/* common */
const __loginAnonymously = (username: string): Promise<User | null> => {
const url = `/auth/anonymous/login?user=${encodeURIComponent(username)}&aud=${siteId}&from=${encodeURIComponent(
`${window.location.origin}${window.location.pathname}?selfClose`
)}`;
return fetcher.get<User>({ url, withCredentials: true, overriddenApiBase: '' });
};
// /auth/anonymous/login?user=sfdfsf&aud=remark&from=https%3A%2F%2Fdemo.remark42.com%2Fweb%2Fiframe.html%3FselfClose&site=remark
// /auth/anonymous/login?site=remark&username=asdasd&aud=remark&from=http://127.0.0.1:9000/web/iframe.html?selfClose
const __loginViaEmail = (token: string): Promise<User | null> => {
const url = `/auth/email/login?token=${token}`;
return fetcher.get<User>({ url, withCredentials: true, overriddenApiBase: '' });
};
function stringifyUrl(url: string, query: Record<string, string | number | undefined>) {
const queryEntries = Object.entries(query);
const siteIdParam = `site=${encodeURIComponent(siteId)}`;
if (queryEntries.length === 0) {
return `${url}?${siteIdParam}`;
}
const queryString = Object.entries(query)
.reduce(
(accum, [k, v]) => (v === undefined ? accum : [...accum, `${encodeURIComponent(k)}=${encodeURIComponent(v)}`]),
[siteIdParam] as string[]
)
.join('&');
return `${url}?${queryString}`;
}
/* common */
const __loginAnonymously = (username: string): Promise<User | null> =>
fetcher.get<User>(
stringifyUrl('/auth/anonymous/login', {
user: username,
aud: siteId,
from: `${window.location.origin}${window.location.pathname}?selfClose`,
})
);
const __loginViaEmail = (token: string): Promise<User | null> =>
fetcher.get<User>(stringifyUrl('/auth/email/login', { token }));
/**
* First step of two of `email` authorization
@@ -22,24 +43,18 @@ const __loginViaEmail = (token: string): Promise<User | null> => {
* @param username userrname
* @param address email address
*/
export const sendEmailVerificationRequest = (username: string, address: string): Promise<void> => {
const url = `/auth/email/login?id=${siteId}&user=${encodeURIComponent(username)}&address=${encodeURIComponent(
address
)}`;
return fetcher.get({ url, withCredentials: true, overriddenApiBase: '' });
};
export const sendEmailVerificationRequest = (username: string, address: string): Promise<void> =>
fetcher.get(stringifyUrl('/auth/email/login', { id: siteId, user: username, address }));
export const logIn = (provider: AuthProvider): Promise<User | null> => {
if (provider.name === 'anonymous') return __loginAnonymously(provider.username);
if (provider.name === 'email') return __loginViaEmail(provider.token);
return new Promise<User | null>((resolve, reject) => {
const url = `${BASE_URL}/auth/${provider.name}/login?from=${encodeURIComponent(
`${window.location.origin}${window.location.pathname}?selfClose`
)}&site=${siteId}`;
const url = stringifyUrl(`${BASE_URL}/auth/${provider.name}/login`, {
from: `${window.location.origin}${window.location.pathname}?selfClose`,
});
const newWindow = window.open(url);
let secondsPass = 0;
const checkMsDelay = 300;
const checkInterval = setInterval(() => {
@@ -64,25 +79,14 @@ export const logIn = (provider: AuthProvider): Promise<User | null> => {
});
};
export const logOut = (): Promise<void> =>
fetcher.get({ url: `/auth/logout`, overriddenApiBase: '', withCredentials: true });
export const logOut = (): Promise<void> => fetcher.get('/auth/logout');
export const getConfig = (): Promise<Config> => fetcher.get(`/config`);
export const getConfig = (): Promise<Config> => fetcher.get('/config');
export const getPostComments = (sort: Sorting) =>
fetcher.get<Tree>({
url: `/find?site=${siteId}&url=${url}&sort=${sort}&format=tree`,
withCredentials: true,
});
fetcher.get<Tree>(stringifyUrl('/find', { url, sort, format: 'tree' }));
export const getCommentsCount = (siteId: string, urls: string[]): Promise<{ url: string; count: number }[]> =>
fetcher.post({
url: `/counts?site=${siteId}`,
body: urls,
});
export const getComment = (id: Comment['id']): Promise<Comment> =>
fetcher.get({ url: `/id/${id}?url=${url}`, withCredentials: true });
export const getComment = (id: Comment['id']): Promise<Comment> => fetcher.get(stringifyUrl(`/id/${id}`, { url }));
export const getUserComments = (
userId: User['id'],
@@ -90,17 +94,10 @@ export const getUserComments = (
): Promise<{
comments: Comment[];
count: number;
}> =>
fetcher.get({
url: `/comments?user=${userId}&limit=${limit}`,
withCredentials: true,
});
}> => fetcher.get(stringifyUrl('/comments', { user: userId, limit }));
export const putCommentVote = ({ id, value }: { id: Comment['id']; value: number }): Promise<void> =>
fetcher.put({
url: `/vote/${id}?url=${url}&vote=${value}`,
withCredentials: true,
});
fetcher.put(stringifyUrl(`/vote/${id}`, { url, vote: value }));
export const addComment = ({
title,
@@ -111,103 +108,47 @@ export const addComment = ({
text: string;
pid?: Comment['id'];
}): Promise<Comment> =>
fetcher.post({
url: '/comment',
body: {
fetcher.post('/comment', {
json: {
title,
text,
locator: {
site: siteId,
url,
},
locator: { site: siteId, url },
...(pid ? { pid } : {}),
},
withCredentials: true,
});
export const updateComment = ({ text, id }: { text: string; id: Comment['id'] }): Promise<Comment> =>
fetcher.put({
url: `/comment/${id}?url=${url}`,
body: {
text,
},
withCredentials: true,
});
fetcher.put(stringifyUrl(`/comment/${id}`, { url }), { json: { text } });
export const getPreview = (text: string): Promise<string> =>
fetcher.post({
url: '/preview',
body: {
text,
},
withCredentials: true,
});
export const getPreview = (text: string): Promise<string> => fetcher.post('/preview', { json: { text } });
export const getUser = (): Promise<User | null> =>
fetcher
.get<User | null>({
url: '/user',
withCredentials: true,
logError: false,
})
.catch(() => null);
export const getUser = (): Promise<User | null> => fetcher.get<User | null>('/user').catch(() => null);
/* GDPR */
export const deleteMe = (): Promise<{
user_id: string;
link: string;
}> =>
fetcher.post({
url: `/deleteme?site=${siteId}`,
withCredentials: true,
});
export const deleteMe = (): Promise<{ user_id: string; link: string }> => fetcher.post('/deleteme');
export const approveDeleteMe = (token: string): Promise<void> =>
fetcher.get({
url: `/admin/deleteme?token=${token}`,
withCredentials: true,
});
fetcher.get(stringifyUrl('/admin/deleteme', { token }));
/* admin */
export const pinComment = (id: Comment['id']): Promise<void> =>
fetcher.put({
url: `/admin/pin/${id}?url=${url}&pin=1`,
withCredentials: true,
});
fetcher.put(stringifyUrl(`/admin/pin/${id}`, { url, pin: 1 }));
export const unpinComment = (id: Comment['id']): Promise<void> =>
fetcher.put({
url: `/admin/pin/${id}?url=${url}&pin=0`,
withCredentials: true,
});
fetcher.put(stringifyUrl(`/admin/pin/${id}`, { url, pin: 0 }));
export const setVerifiedStatus = (id: User['id']): Promise<void> =>
fetcher.put({
url: `/admin/verify/${id}?verified=1`,
withCredentials: true,
});
fetcher.put(stringifyUrl(`/admin/verify/${id}`, { verified: 1 }));
export const removeVerifiedStatus = (id: User['id']): Promise<void> =>
fetcher.put({
url: `/admin/verify/${id}?verified=0`,
withCredentials: true,
});
fetcher.put(stringifyUrl(`/admin/verify/${id}`, { verified: 0 }));
export const removeComment = (id: Comment['id']) =>
fetcher.delete({
url: `/admin/comment/${id}?url=${url}`,
withCredentials: true,
});
export const removeComment = (id: Comment['id']): Promise<void> =>
fetcher.delete(stringifyUrl(`/admin/comment/${id}`, { url }));
export const removeMyComment = (id: Comment['id']): Promise<void> =>
fetcher.put({
url: `/comment/${id}?url=${url}`,
body: {
delete: true,
} as object,
withCredentials: true,
});
fetcher.put(stringifyUrl(`/comment/${id}`, { url }), { json: { delete: true } });
export const blockUser = (
id: User['id'],
@@ -216,11 +157,7 @@ export const blockUser = (
block: boolean;
site_id: string;
user_id: string;
}> =>
fetcher.put({
url: ttl === 'permanently' ? `/admin/user/${id}?block=1` : `/admin/user/${id}?block=1&ttl=${ttl}`,
withCredentials: true,
});
}> => fetcher.put(stringifyUrl(`/admin/user/${id}`, { block: 1, ttl: ttl === 'permanently' ? ttl : undefined }));
export const unblockUser = (
id: User['id']
@@ -228,39 +165,23 @@ export const unblockUser = (
block: boolean;
site_id: string;
user_id: string;
}> =>
fetcher.put({
url: `/admin/user/${id}?block=0`,
withCredentials: true,
});
}> => fetcher.put(stringifyUrl(`/admin/user/${id}`, { block: 0 }));
export const getBlocked = (): Promise<BlockedUser[] | null> =>
fetcher.get({
url: '/admin/blocked',
withCredentials: true,
});
export const getBlocked = (): Promise<BlockedUser[] | null> => fetcher.get('/admin/blocked');
export const disableComments = (): Promise<void> =>
fetcher.put({
url: `/admin/readonly?site=${siteId}&url=${url}&ro=1`,
withCredentials: true,
});
export const disableComments = (): Promise<void> => fetcher.put(stringifyUrl('/admin/readonly', { url, ro: 1 }));
export const enableComments = (): Promise<void> =>
fetcher.put({
url: `/admin/readonly?site=${siteId}&url=${url}&ro=0`,
withCredentials: true,
});
export const enableComments = (): Promise<void> => fetcher.put(stringifyUrl('/admin/readonly', { url, ro: 0 }));
export const uploadImage = (image: File): Promise<Image> => {
const data = new FormData();
data.append('file', image);
return fetcher
.post<{ id: string }>({
url: `/picture`,
withCredentials: true,
contentType: 'multipart/form-data',
.post<{ id: string }>('/picture', {
headers: {
'Content-Type': 'multipart/form-data',
},
body: data,
})
.then((resp) => ({
@@ -276,19 +197,16 @@ export const uploadImage = (image: File): Promise<Image> => {
* @param emailAddress email for subscription
*/
export const emailVerificationForSubscribe = (emailAddress: string) =>
fetcher.post({
url: `/email/subscribe?site=${siteId}&address=${encodeURIComponent(emailAddress)}`,
withCredentials: true,
});
fetcher.post(stringifyUrl('/email/subscribe', { address: emailAddress }));
/**
* Confirmation of email subscription to updates
* @param token confirmation token from email
*/
export const emailConfirmationForSubscribe = (token: string) =>
fetcher.post({ url: `/email/confirm?site=${siteId}&tkn=${encodeURIComponent(token)}`, withCredentials: true });
fetcher.post(stringifyUrl('/email/confirm', { tkn: token }));
/**
* Decline current subscription to updates
*/
export const unsubscribeFromEmailUpdates = () => fetcher.delete({ url: `/email`, withCredentials: true });
export const unsubscribeFromEmailUpdates = () => fetcher.delete('/email');
-4
View File
@@ -33,11 +33,7 @@ export const LS_SORT_KEY = '__remarkSort';
/** localstorage key for email of logged in user */
export const LS_EMAIL_KEY = '__remarkEmail';
/** Header name for jwt token */
export const HEADER_X_JWT = 'X-JWT';
export const THEMES: Theme[] = ['light', 'dark'];
export const IS_MOBILE = /Android|webOS|iPhone|iPad|iPod|Opera Mini|Windows Phone/i.test(navigator.userAgent);
/**
+186 -46
View File
@@ -1,73 +1,213 @@
import fetcher from './fetcher';
import { RequestError } from 'utils/errorUtils';
import { API_BASE, BASE_URL } from './constants.config';
import fetcher, { JWT_HEADER, XSRF_HEADER } from './fetcher';
type FetchImplementaitonProps = {
status?: number;
headers?: Record<string, string>;
json?: () => Promise<unknown>;
text?: () => Promise<unknown>;
data?: unknown;
};
function mockFetch({ headers = {}, data = {}, ...props }: FetchImplementaitonProps = {}) {
window.fetch = jest.fn().mockImplementation(() => {
return {
status: 200,
headers: new Headers(headers),
async json() {
return data;
},
async text() {
return JSON.stringify(data);
},
...props,
};
});
}
describe('fetcher', () => {
describe('errors', () => {
const headers = { [XSRF_HEADER]: '' };
describe('methods', () => {
it('should send GET request', async () => {
expect.assertions(1);
mockFetch();
await fetcher.get('/auth');
expect(window.fetch).toHaveBeenCalledWith(`${BASE_URL}/auth`, { method: 'get', headers });
});
it('should send POST request', async () => {
expect.assertions(1);
mockFetch();
await fetcher.post('/auth');
expect(window.fetch).toHaveBeenCalledWith(`${BASE_URL}/auth`, { method: 'post', headers });
});
it('should send PUT request', async () => {
expect.assertions(1);
mockFetch();
await fetcher.put('/auth');
expect(window.fetch).toHaveBeenCalledWith(`${BASE_URL}/auth`, { method: 'put', headers });
});
it('should send DELETE request', async () => {
expect.assertions(1);
mockFetch();
await fetcher.delete('/auth');
expect(window.fetch).toHaveBeenCalledWith(`${BASE_URL}/auth`, { method: 'delete', headers });
});
});
describe('endpoint formation', () => {
it("shouldn't add API_BASE for requests to auth endpoints", async () => {
expect.assertions(1);
mockFetch();
await fetcher.post('/auth/google/login');
expect(window.fetch).toHaveBeenCalledWith(`${BASE_URL}/auth/google/login`, { method: 'post', headers });
});
it('should add API_BASE for requests to auth endpoints', async () => {
expect.assertions(1);
mockFetch();
await fetcher.post('/comments');
expect(window.fetch).toHaveBeenCalledWith(`${BASE_URL}${API_BASE}/comments`, { method: 'post', headers });
});
});
describe('headers', () => {
it('should set active token and than clean it on unauthorized respose', async () => {
expect.assertions(4);
const headersWithJwt = { [JWT_HEADER]: 'token', ...headers };
// Set token to `activeJwtToken`
mockFetch({ headers: headersWithJwt });
await fetcher.get('/comments');
expect(window.fetch).toHaveBeenCalled();
// Check if `activeJwtToken` saved and clean
mockFetch({ headers, status: 401 });
await fetcher
.get('/comments')
.then(() => {
throw Error('Fetcher shoud throw error on 401 responce');
})
.catch((e) => {
expect(e.message).toBe('Not authorized.');
});
expect(window.fetch).toHaveBeenCalledWith(`${BASE_URL}${API_BASE}/comments`, {
method: 'get',
headers: headersWithJwt,
});
// Check if `activeJwtToken` was cleaned
mockFetch({ headers });
await fetcher.get('/comments', { headers });
expect(window.fetch).toHaveBeenCalledWith(`${BASE_URL}${API_BASE}/comments`, { method: 'get', headers });
});
});
describe('send data', () => {
it('should send JSON', async () => {
expect.assertions(1);
const json = { text: 'text' };
const headersWithContentType = { ...headers, 'Content-Type': 'application/json' };
mockFetch();
await fetcher.post('/comment', { json });
expect(window.fetch).toBeCalledWith(`${BASE_URL}${API_BASE}/comment`, {
method: 'post',
headers: headersWithContentType,
body: JSON.stringify(json),
});
});
it('should send form data', async () => {
expect.assertions(1);
const headersWithMultipartData = { ...headers, 'Content-Type': 'multipart/form-data' };
const body = new FormData();
mockFetch();
await fetcher.post('/comment', { body, headers: headersWithMultipartData });
expect(window.fetch).toHaveBeenCalledWith(`${BASE_URL}${API_BASE}/comment`, {
method: 'post',
body,
headers: headersWithMultipartData,
});
});
});
describe('request errors', () => {
it('should throw json on api json response with >= 400 status code', async () => {
const response = {
expect.assertions(1);
const data = {
code: 2,
error: 'you just cant',
details: 'you just cant at all',
};
window.fetch = jest.fn().mockImplementation(async () => ({
status: 400,
headers: new Headers(),
json: async () => response,
text: async () => JSON.stringify(response),
}));
return fetcher
.get('/api/some')
.then((data) => {
throw new Error('Request should be failed');
})
.catch((e) => {
expect(e.code).toBe(2);
expect(e.error).toBe('you just cant');
expect(e.details).toBe('you just cant at all');
});
mockFetch({ status: 400, data });
await expect(fetcher.get('/anything')).rejects.toEqual(data);
});
it('should throw error on api json response with >= 400 status code and bad json from server', async () => {
expect.assertions(1);
const data = ']{{"code: 2';
mockFetch({ status: 400, data });
await expect(fetcher.get('/anything')).rejects.toEqual(data);
});
it('should throw special error object on 401 status', async () => {
expect.assertions(1);
const response = '<html>unauthorized nginx response</html>';
window.fetch = jest.fn().mockImplementation(async () => ({
mockFetch({
status: 401,
headers: new Headers(),
json: async () => {
json() {
throw new Error('json parse error');
},
text: async () => response,
}));
async text() {
return response;
},
});
return fetcher
.get('/api/some')
.then((data) => {
throw new Error('Request should be failed');
})
.catch((e) => {
expect(e.code).toBe(401);
expect(e.error).toBe('Not authorized.');
});
await expect(fetcher.get('/anything')).rejects.toEqual(new RequestError('Not authorized.', 401));
});
it('should throw "Something went wrong." object on unknown status', async () => {
window.fetch = jest.fn().mockImplementation(async () => ({
expect.assertions(1);
mockFetch({
status: 400,
headers: new Headers(),
async json() {
json() {
throw new Error('json parse error');
},
async text() {
return 'you given me something wrong';
},
}));
});
return fetcher
.get({ url: '/api/some', logError: false })
.then((data) => {
throw new Error('Request should be failed');
})
.catch((e) => {
expect(e.code).toBe(0);
expect(e.error).toBe('Something went wrong.');
});
await expect(fetcher.get('/anything')).rejects.toEqual(new RequestError('Something went wrong.', 0));
});
});
});
+68 -109
View File
@@ -1,139 +1,98 @@
import { httpErrorMap, isFailedFetch, httpMessages, RequestError } from 'utils/errorUtils';
import { httpErrorMap, httpMessages, RequestError } from 'utils/errorUtils';
import { BASE_URL, API_BASE, HEADER_X_JWT } from './constants';
import { siteId } from './settings';
import { BASE_URL, API_BASE } from './constants';
import { StaticStore } from './static-store';
import { getCookie } from './cookies';
export type FetcherMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head';
const methods: FetcherMethod[] = ['get', 'post', 'put', 'patch', 'delete', 'head'];
/** List of fetchers supported methods */
const METHODS = ['get', 'post', 'put', 'delete'] as const;
/** Header name for JWT token */
export const JWT_HEADER = 'X-JWT';
/** Header name for XSRF token */
export const XSRF_HEADER = 'X-XSRF-TOKEN';
/** Cookie field with XSRF token */
export const XSRF_COOKIE = 'XSRF-TOKEN';
interface FetcherInitBase {
url: string;
overriddenApiBase?: string;
withCredentials?: boolean;
/** whether log error message to console */
logError?: boolean;
}
interface FetcherInitJSON extends FetcherInitBase {
contentType?: 'application/json';
body?: string | object | Blob | ArrayBuffer;
}
interface FetcherInitMultipart extends FetcherInitBase {
contentType: 'multipart/form-data';
body: FormData;
}
type FetcherInit = string | FetcherInitJSON | FetcherInitMultipart;
type FetcherObject = { [K in FetcherMethod]: <T = unknown>(data: FetcherInit) => Promise<T> };
type Methods = typeof METHODS;
type FetchInit = Omit<RequestInit, 'headers'> & {
headers?: Record<string, string>;
json?: unknown;
query?: Record<string, string | number | undefined>;
};
type FetcherObject = Record<Methods[number], <T>(url: string, params?: FetchInit) => Promise<T>>;
/** JWT token received from server and will be send by each request, if it present */
let activeJwtToken: string | undefined;
const fetcher = methods.reduce<Partial<FetcherObject>>((acc, method) => {
acc[method] = async <T = unknown>(data: FetcherInit): Promise<T> => {
const {
url,
body = undefined,
withCredentials = false,
overriddenApiBase = API_BASE,
contentType = 'application/json',
logError = true,
} = typeof data === 'string' ? { url: data } : data;
const baseUrl = `${BASE_URL}${overriddenApiBase}`;
const headers = new Headers({
Accept: 'application/json',
'X-XSRF-TOKEN': getCookie('XSRF-TOKEN') || '',
});
if (contentType !== 'multipart/form-data') {
headers.append('Content-Type', contentType);
}
const fetcher = METHODS.reduce((acc, method) => {
acc[method] = async (uri: string, params: FetchInit = {}) => {
const { headers = {}, json, ...fetchParams } = params;
// add api base if it's not auth request
// we use `indexOf` instead of `startsWidth` because we don't want to have another one polyfill for no reason
const baseUrl = uri.indexOf('/auth') === 0 ? BASE_URL : `${BASE_URL}${API_BASE}`;
const url = `${baseUrl}${uri}`;
// Save token in memory and pass it into headers in case if storing cookies is disabled
if (activeJwtToken) {
headers.append(HEADER_X_JWT, activeJwtToken);
headers[JWT_HEADER] = activeJwtToken;
}
headers[XSRF_HEADER] = getCookie(XSRF_COOKIE) || '';
if (json) {
headers['Content-Type'] = 'application/json';
fetchParams.body = JSON.stringify(json);
}
let rurl = `${baseUrl}${url}`;
try {
const res = await fetch(url, { ...fetchParams, method, headers });
// TODO: it should be clarified when frontend gets this header and what could be in it to simplify this logic and cover by tests
const date = (res.headers.has('date') && res.headers.get('date')) || '';
const timestamp = isNaN(Date.parse(date)) ? 0 : Date.parse(date);
const timeDiff = (new Date().getTime() - timestamp) / 1000;
const parameters: RequestInit = {
method,
headers,
mode: 'cors',
credentials: withCredentials ? 'include' : 'omit',
};
StaticStore.serverClientTimeDiff = timeDiff;
if (body) {
if (contentType === 'multipart/form-data') {
parameters.body = body as FormData;
} else if (typeof body === 'object' && !(body instanceof Blob) && !(body instanceof ArrayBuffer)) {
parameters.body = JSON.stringify(body);
} else {
parameters.body = body;
// backend could update jwt in any time. so, we should handle it
if (res.headers.has(JWT_HEADER)) {
activeJwtToken = res.headers.get(JWT_HEADER) as string;
}
}
if (siteId && method !== 'post' && !rurl.includes('?site=') && !rurl.includes('&site=')) {
rurl += `${rurl.includes('?') ? '&' : '?'}site=${siteId}`;
}
if ([401, 403].includes(res.status)) {
activeJwtToken = undefined;
}
return fetch(rurl, parameters)
.then((res) => {
const date = (res.headers.has('date') && res.headers.get('date')) || '';
const timestamp = isNaN(Date.parse(date)) ? 0 : Date.parse(date);
const timeDiff = (new Date().getTime() - timestamp) / 1000;
StaticStore.serverClientTimeDiff = timeDiff;
if (res.status >= 400) {
if (httpErrorMap.has(res.status)) {
const descriptor = httpErrorMap.get(res.status) || httpMessages.unexpectedError;
// backend could update jwt in any time. so, we should handle it
if (res.headers.has(HEADER_X_JWT)) {
activeJwtToken = res.headers.get(HEADER_X_JWT) as string;
throw new RequestError(descriptor.defaultMessage, res.status);
}
if (res.status === 403 && activeJwtToken) {
activeJwtToken = undefined;
}
if (res.status >= 400) {
if (httpErrorMap.has(res.status)) {
const descriptor = httpErrorMap.get(res.status) || httpMessages.unexpectedError;
throw new RequestError(descriptor.defaultMessage, res.status);
return res.text().then((text) => {
let err;
try {
err = JSON.parse(text);
} catch (e) {
throw new RequestError(httpMessages.unexpectedError.defaultMessage, 0);
}
return res.text().then((text) => {
let err;
try {
err = JSON.parse(text);
} catch (e) {
if (logError) {
console.error(err);
}
throw err;
});
}
throw new RequestError(httpMessages.unexpectedError.defaultMessage, 0);
}
throw err;
});
}
if (res.headers.get('Content-Type')?.indexOf('application/json') === 0) {
return res.json();
}
if (res.headers.get('Content-Type')?.startsWith('application/json')) {
return res.json();
}
return res.text();
} catch (e) {
if (e?.message === 'Failed to fetch') {
throw new RequestError(e.message, -2);
}
return res.text();
})
.catch((e) => {
if (isFailedFetch(e)) {
throw new RequestError(e.message, -2);
}
throw e;
});
throw e;
}
};
return acc;
}, {}) as FetcherObject;
}, {} as FetcherObject);
export default fetcher;
-4
View File
@@ -181,10 +181,6 @@ export const httpErrorMap = new Map([
[429, httpMessages.toManyRequest],
]);
export function isFailedFetch(e?: Error): boolean {
return Boolean(e && e.message && e.message === `Failed to fetch`);
}
export type FetcherError =
| string
| {