From 2fae0c29d2665538b7b8620bfb3f65a47244501d Mon Sep 17 00:00:00 2001 From: Pavel Mineev Date: Sat, 30 Jan 2021 23:43:44 +0300 Subject: [PATCH] Update fetcher and add good coverage to it --- frontend/app/__mocks__/headers.ts | 14 ++ frontend/app/common/api.ts | 222 +++++++++----------------- frontend/app/common/constants.ts | 4 - frontend/app/common/fetcher.test.ts | 232 ++++++++++++++++++++++------ frontend/app/common/fetcher.ts | 177 ++++++++------------- frontend/app/utils/errorUtils.ts | 4 - 6 files changed, 338 insertions(+), 315 deletions(-) diff --git a/frontend/app/__mocks__/headers.ts b/frontend/app/__mocks__/headers.ts index 0efddd1d..48746784 100644 --- a/frontend/app/__mocks__/headers.ts +++ b/frontend/app/__mocks__/headers.ts @@ -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); } diff --git a/frontend/app/common/api.ts b/frontend/app/common/api.ts index 3026bf73..c6626be7 100644 --- a/frontend/app/common/api.ts +++ b/frontend/app/common/api.ts @@ -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 => { - const url = `/auth/anonymous/login?user=${encodeURIComponent(username)}&aud=${siteId}&from=${encodeURIComponent( - `${window.location.origin}${window.location.pathname}?selfClose` - )}`; - return fetcher.get({ 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 => { - const url = `/auth/email/login?token=${token}`; - return fetcher.get({ url, withCredentials: true, overriddenApiBase: '' }); -}; +function stringifyUrl(url: string, query: Record) { + 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 => + fetcher.get( + stringifyUrl('/auth/anonymous/login', { + user: username, + aud: siteId, + from: `${window.location.origin}${window.location.pathname}?selfClose`, + }) + ); + +const __loginViaEmail = (token: string): Promise => + fetcher.get(stringifyUrl('/auth/email/login', { token })); /** * First step of two of `email` authorization @@ -22,24 +43,18 @@ const __loginViaEmail = (token: string): Promise => { * @param username userrname * @param address email address */ -export const sendEmailVerificationRequest = (username: string, address: string): Promise => { - 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 => + fetcher.get(stringifyUrl('/auth/email/login', { id: siteId, user: username, address })); export const logIn = (provider: AuthProvider): Promise => { if (provider.name === 'anonymous') return __loginAnonymously(provider.username); if (provider.name === 'email') return __loginViaEmail(provider.token); return new Promise((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 => { }); }; -export const logOut = (): Promise => - fetcher.get({ url: `/auth/logout`, overriddenApiBase: '', withCredentials: true }); +export const logOut = (): Promise => fetcher.get('/auth/logout'); -export const getConfig = (): Promise => fetcher.get(`/config`); +export const getConfig = (): Promise => fetcher.get('/config'); export const getPostComments = (sort: Sorting) => - fetcher.get({ - url: `/find?site=${siteId}&url=${url}&sort=${sort}&format=tree`, - withCredentials: true, - }); + fetcher.get(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 => - fetcher.get({ url: `/id/${id}?url=${url}`, withCredentials: true }); +export const getComment = (id: Comment['id']): Promise => 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 => - 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 => - 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 => - 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 => - fetcher.post({ - url: '/preview', - body: { - text, - }, - withCredentials: true, - }); +export const getPreview = (text: string): Promise => fetcher.post('/preview', { json: { text } }); -export const getUser = (): Promise => - fetcher - .get({ - url: '/user', - withCredentials: true, - logError: false, - }) - .catch(() => null); +export const getUser = (): Promise => fetcher.get('/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 => - fetcher.get({ - url: `/admin/deleteme?token=${token}`, - withCredentials: true, - }); + fetcher.get(stringifyUrl('/admin/deleteme', { token })); /* admin */ export const pinComment = (id: Comment['id']): Promise => - 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 => - 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 => - 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 => - 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 => + fetcher.delete(stringifyUrl(`/admin/comment/${id}`, { url })); export const removeMyComment = (id: Comment['id']): Promise => - 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 => - fetcher.get({ - url: '/admin/blocked', - withCredentials: true, - }); +export const getBlocked = (): Promise => fetcher.get('/admin/blocked'); -export const disableComments = (): Promise => - fetcher.put({ - url: `/admin/readonly?site=${siteId}&url=${url}&ro=1`, - withCredentials: true, - }); +export const disableComments = (): Promise => fetcher.put(stringifyUrl('/admin/readonly', { url, ro: 1 })); -export const enableComments = (): Promise => - fetcher.put({ - url: `/admin/readonly?site=${siteId}&url=${url}&ro=0`, - withCredentials: true, - }); +export const enableComments = (): Promise => fetcher.put(stringifyUrl('/admin/readonly', { url, ro: 0 })); export const uploadImage = (image: File): Promise => { 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 => { * @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'); diff --git a/frontend/app/common/constants.ts b/frontend/app/common/constants.ts index 144dad4c..aede476a 100644 --- a/frontend/app/common/constants.ts +++ b/frontend/app/common/constants.ts @@ -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); /** diff --git a/frontend/app/common/fetcher.test.ts b/frontend/app/common/fetcher.test.ts index 019f5e10..d0d84c71 100644 --- a/frontend/app/common/fetcher.test.ts +++ b/frontend/app/common/fetcher.test.ts @@ -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; + json?: () => Promise; + text?: () => Promise; + 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 = 'unauthorized nginx response'; - 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)); }); }); }); diff --git a/frontend/app/common/fetcher.ts b/frontend/app/common/fetcher.ts index fb8caaef..6c65c6f1 100644 --- a/frontend/app/common/fetcher.ts +++ b/frontend/app/common/fetcher.ts @@ -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 fetcher’s 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]: (data: FetcherInit) => Promise }; +type Methods = typeof METHODS; +type FetchInit = Omit & { + headers?: Record; + json?: unknown; + query?: Record; +}; +type FetcherObject = Record(url: string, params?: FetchInit) => Promise>; /** JWT token received from server and will be send by each request, if it present */ let activeJwtToken: string | undefined; -const fetcher = methods.reduce>((acc, method) => { - acc[method] = async (data: FetcherInit): Promise => { - 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; diff --git a/frontend/app/utils/errorUtils.ts b/frontend/app/utils/errorUtils.ts index 26d79cc5..fb76c808 100644 --- a/frontend/app/utils/errorUtils.ts +++ b/frontend/app/utils/errorUtils.ts @@ -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 | {