Skip to content

Commit

Permalink
api-client cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
j8seangel committed Aug 14, 2024
1 parent 764ada6 commit 3b245e1
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 88 deletions.
2 changes: 1 addition & 1 deletion apps/fishing-map/features/workspace/workspace.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ interface WorkspaceSliceState {
const initialState: WorkspaceSliceState = {
status: AsyncReducerStatus.Idle,
customStatus: AsyncReducerStatus.Idle,
error: {},
error: {} as AsyncError,
data: null,
password: '',
lastVisited: undefined,
Expand Down
9 changes: 4 additions & 5 deletions apps/fishing-map/utils/async-slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
createEntityAdapter,
IdSelector,
} from '@reduxjs/toolkit'
import { ResponseError } from '@globalfishingwatch/api-client'

export enum AsyncReducerStatus {
Idle = 'idle',
Expand All @@ -19,9 +20,7 @@ export enum AsyncReducerStatus {
Error = 'error',
}

export type AsyncError<Metadata = Record<string, any>> = {
status?: number // HHTP error codes
message?: string
export type AsyncError<Metadata = Record<string, any>> = ResponseError & {
metadata?: Metadata
}

Expand All @@ -38,7 +37,7 @@ export type AsyncReducer<T = any> = {
export const asyncInitialState: AsyncReducer = {
status: AsyncReducerStatus.Idle,
statusId: null,
error: {},
error: {} as AsyncError,
ids: [],
currentRequestIds: [],
entities: {},
Expand All @@ -55,7 +54,7 @@ const getRequestIdsOnFinish = (currentRequestIds: string[], action: any) => {
export const createAsyncSlice = <
T,
U extends { id: AsyncReducerId },
Reducers extends SliceCaseReducers<T> = SliceCaseReducers<T>,
Reducers extends SliceCaseReducers<T> = SliceCaseReducers<T>
>({
name = '',
initialState = {} as T,
Expand Down
89 changes: 14 additions & 75 deletions libs/api-client/src/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,18 @@ import {
UserPermission,
} from '@globalfishingwatch/api-types'
import { isUrlAbsolute } from './utils/url'
import { isAuthError, parseAPIError } from './utils/errors'

export const API_GATEWAY =
process.env.API_GATEWAY ||
process.env.REACT_APP_API_GATEWAY ||
process.env.NEXT_PUBLIC_API_GATEWAY ||
'https://gateway.api.dev.globalfishingwatch.org'

export const USER_TOKEN_STORAGE_KEY = 'GFW_API_USER_TOKEN'
export const USER_REFRESH_TOKEN_STORAGE_KEY = 'GFW_API_USER_REFRESH_TOKEN'
export const API_VERSION = process.env.NEXT_PUBLIC_API_VERSION || 'v3'

const DEBUG_API_REQUESTS: boolean = process.env.NEXT_PUBLIC_DEBUG_API_REQUESTS === 'true'
const AUTH_PATH = 'auth'
const REGISTER_PATH = 'registration'
export const GUEST_USER_TYPE = 'guest'

export type V2MetadataError = Record<string, any>
export interface V2MessageError {
detail: string
title: string
metadata?: V2MetadataError
}
export interface ResponseError {
status: number
message: string
messages?: V2MessageError[]
}
import { getIsUnauthorizedError, isAuthError, parseAPIError } from './utils/errors'
import {
API_GATEWAY,
API_VERSION,
AUTH_PATH,
DEBUG_API_REQUESTS,
GUEST_USER_TYPE,
REGISTER_PATH,
USER_REFRESH_TOKEN_STORAGE_KEY,
USER_TOKEN_STORAGE_KEY,
} from './config'
import { parseJSON, processStatus } from './utils/parse'

interface UserTokens {
token: string
Expand Down Expand Up @@ -62,51 +46,6 @@ interface LibConfig {
baseUrl?: string
}

const processStatus = (
response: Response,
requestStatus?: ResourceResponseType
): Promise<Response> => {
return new Promise(async (resolve, reject) => {
const { status, statusText } = response
try {
if (response.status >= 200 && response.status < 400) {
return resolve(response)
}

if (requestStatus === 'default') {
return reject(response)
}
// Compatibility with v1 and v2 errors format
const errors = {
message: '',
messages: [],
}
if (response.status >= 400 && response.status < 500) {
await response.text().then((text) => {
try {
const res = JSON.parse(text)
errors.message = res.message
errors.messages = res.messages
} catch (e: any) {
errors.message = statusText
}
})
}
return reject({
status,
message: errors?.message || statusText,
messages: errors.messages,
})
} catch (e: any) {
return reject({ status, message: statusText })
}
})
}

const parseJSON = (response: Response) => response.json()
const isUnauthorizedError = (error: ResponseError) =>
error && error.status > 400 && error.status < 403

const isClientSide = typeof window !== 'undefined'

export type RequestStatus = 'idle' | 'refreshingToken' | 'logging' | 'downloading'
Expand Down Expand Up @@ -514,7 +453,7 @@ export class GFW_API_CLASS {
}
} catch (e: any) {
if (!this.getToken() && !this.getRefreshToken()) {
const msg = isUnauthorizedError(e)
const msg = getIsUnauthorizedError(e)
? 'Invalid access token'
: 'Error trying to generate tokens'
if (this.debug) {
Expand Down Expand Up @@ -567,7 +506,7 @@ export class GFW_API_CLASS {
this.status = 'idle'
return user
} catch (e: any) {
const msg = isUnauthorizedError(e)
const msg = getIsUnauthorizedError(e)
? 'Invalid refresh token'
: 'Error trying to refreshing the token'
console.warn(e)
Expand Down
15 changes: 15 additions & 0 deletions libs/api-client/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const API_GATEWAY =
process.env.API_GATEWAY ||
process.env.REACT_APP_API_GATEWAY ||
process.env.NEXT_PUBLIC_API_GATEWAY ||
'https://gateway.api.dev.globalfishingwatch.org'

export const USER_TOKEN_STORAGE_KEY = 'GFW_API_USER_TOKEN'
export const USER_REFRESH_TOKEN_STORAGE_KEY = 'GFW_API_USER_REFRESH_TOKEN'
export const API_VERSION = process.env.NEXT_PUBLIC_API_VERSION || 'v3'

export const DEBUG_API_REQUESTS: boolean = process.env.NEXT_PUBLIC_DEBUG_API_REQUESTS === 'true'
export const AUTH_PATH = 'auth'
export const REGISTER_PATH = 'registration'
export const GUEST_USER_TYPE = 'guest'
export const CONCURRENT_ERROR_STATUS = 429
1 change: 1 addition & 0 deletions libs/api-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export * from './utils/url'
export * from './utils/search'
export * from './utils/errors'
export * from './utils/thinning'
export * from './config'
export * from './api-client'
38 changes: 31 additions & 7 deletions libs/api-client/src/utils/errors.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,48 @@
import { ResponseError, V2MetadataError } from '../api-client'
import { CONCURRENT_ERROR_STATUS } from '../config'

export type V2MetadataError = Record<string, any>
export interface V2MessageError {
detail: string
title: string
metadata?: V2MetadataError
}
export interface ResponseError {
status: number
message: string
messages?: V2MessageError[]
}

export const getIsUnauthorizedError = (error?: ResponseError | { status?: number }) =>
error && error.status && error.status > 400 && error.status < 403

export const getIsConcurrentError = (error?: ResponseError | { status?: number }) =>
error?.status === CONCURRENT_ERROR_STATUS

// The 524 timeout from cloudfare is not handled properly
// and rejects with a typeError
export const crossBrowserTypeErrorMessages = [
'Load failed', // Safari
'Failed to fetch', // Chromium
]
export const getIsTimeoutError = (error?: ResponseError | { message?: string }) => {
if (!error?.message) return false
return crossBrowserTypeErrorMessages.some((e) => e.includes(error?.message as string))
}

export const parseAPIErrorStatus = (error: ResponseError) => {
return error.status || (error as any).code || null
return error?.status || (error as any).code || null
}

export const parseAPIErrorMessage = (error: ResponseError) => {
if (error.messages?.length) {
return error.messages[0]?.detail
if (error?.messages?.length) {
return error?.messages[0]?.detail
}
return error.message || ''
return error?.message || ''
}

export const parseAPIErrorMetadata = (error: ResponseError) => {
if (error.messages?.length) {
return error.messages[0]?.metadata
if (error?.messages?.length) {
return error?.messages[0]?.metadata
}
return {} as V2MetadataError
}
Expand Down
44 changes: 44 additions & 0 deletions libs/api-client/src/utils/parse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { ResourceResponseType } from '@globalfishingwatch/api-types'

export const processStatus = (
response: Response,
requestStatus?: ResourceResponseType
): Promise<Response> => {
return new Promise(async (resolve, reject) => {
const { status, statusText } = response
try {
if (response.status >= 200 && response.status < 400) {
return resolve(response)
}

if (requestStatus === 'default') {
return reject(response)
}
// Compatibility with v1 and v2 errors format
const errors = {
message: '',
messages: [],
}
if (response.status >= 400 && response.status < 500) {
await response.text().then((text) => {
try {
const res = JSON.parse(text)
errors.message = res.message
errors.messages = res.messages
} catch (e: any) {
errors.message = statusText
}
})
}
return reject({
status,
message: errors?.message || statusText,
messages: errors.messages,
})
} catch (e: any) {
return reject({ status, message: statusText })
}
})
}

export const parseJSON = (response: Response) => response.json()

0 comments on commit 3b245e1

Please sign in to comment.