diff --git a/src/api/base/fetchClient.ts b/src/api/base/fetchClient.ts index ddb7f27b..fa7b51e7 100644 --- a/src/api/base/fetchClient.ts +++ b/src/api/base/fetchClient.ts @@ -1,4 +1,14 @@ import { QueryClient } from '@tanstack/react-query'; +import { randomUUID } from 'crypto'; + +export class HTTPError extends Error { + constructor( + readonly response: Response, + readonly requestId: string, + ) { + super(`HTTP ${response.status} ${response.statusText}`); + } +} export const DEFAULT_CACHE_TIME = 10 * 1000; @@ -17,7 +27,10 @@ export const cancelActiveRequestsAndInvalidateCache = () => { export async function fetchClient(url: RequestInfo, opts: RequestInit = {}) { const headers = opts.headers instanceof Headers ? opts.headers : new Headers(opts.headers ?? {}); - const requestId = headers.get('x-request-id') ?? ''; + if (!headers.has('x-request-id')) { + headers.set('x-request-id', randomUUID()); + } + const requestId = headers.get('x-request-id')!; let urlString; let method; @@ -32,7 +45,12 @@ export async function fetchClient(url: RequestInfo, opts: RequestInit = {}) { console.log(`${method} ${url.url} x-request-id: ${requestId}`); } - const queryFn = ({ signal }: { signal?: AbortSignal } = {}) => fetch(url, { ...opts, signal }); + if (method === 'POST' && opts.body && !headers.has('Content-Type')) { + headers.set('Content-Type', 'application/json'); + } + + const queryFn = ({ signal }: { signal?: AbortSignal } = {}) => + fetch(url, { ...opts, headers, signal }); let response; @@ -58,6 +76,10 @@ export async function fetchClient(url: RequestInfo, opts: RequestInit = {}) { response.headers.append('x-request-id', requestId); } + if (!response.ok) { + throw new HTTPError(response, requestId); + } + return response; } diff --git a/src/api/fetchRawNftMetadata.ts b/src/api/fetchRawNftMetadata.ts index 970a009b..1dfbc87e 100644 --- a/src/api/fetchRawNftMetadata.ts +++ b/src/api/fetchRawNftMetadata.ts @@ -1,5 +1,5 @@ import { getHarmony } from '@/api/base/apiFactory'; -import { fetchClient } from '@/api/base/fetchClient'; +import { fetchClient, HTTPError } from '@/api/base/fetchClient'; import type { NftWithRawMetadata, RawNftMetadata } from '@/api/fetchNfts'; import type { NFT } from '@/api/types'; @@ -17,8 +17,14 @@ export async function fetchRawNftMetadata(nft: NFT): Promise if (result.content && 'isNFT' in result.content && result.content.isNFT) { metadata = result.content ?? undefined; if (!metadata?.contentType && metadata?.contentUrl) { - const contentTypeResult = await fetchClient(metadata?.contentUrl, { method: 'HEAD' }); - metadata.contentType = contentTypeResult.headers.get('content-type')?.toLowerCase(); + try { + const contentTypeResult = await fetchClient(metadata?.contentUrl, { method: 'HEAD' }); + metadata.contentType = contentTypeResult.headers.get('content-type')?.toLowerCase(); + } catch (e) { + if (!(e instanceof HTTPError)) { + throw e; + } + } } } return { @@ -27,6 +33,9 @@ export async function fetchRawNftMetadata(nft: NFT): Promise metadataType: 'raw', }; } catch (e) { + if (e instanceof HTTPError) { + console.error('HTTPError while fetching metadata', await e.response.text()); + } handleError(e, 'ERROR_CONTEXT_PLACEHOLDER'); return { metadata, diff --git a/src/api/krakenConnect/base/fetchFastApiKeys.ts b/src/api/krakenConnect/base/fetchFastApiKeys.ts index c0e42683..11c04752 100644 --- a/src/api/krakenConnect/base/fetchFastApiKeys.ts +++ b/src/api/krakenConnect/base/fetchFastApiKeys.ts @@ -2,6 +2,7 @@ import { KRAKEN_CONNECT_PERMISSIONS } from '../consts'; import { KRAKEN_API_URI, KRAKEN_CONNECT_CLIENT_ID, URLs } from '/config'; import { handleError } from '/helpers/errorHandler'; +import { fetchClient, HTTPError } from '@/api/base/fetchClient'; export type ApiKeyResponse = { api_key: string; @@ -30,7 +31,7 @@ export async function fetchFastApiKey(code: string, verification: string): Promi body: tokenBodyParams.toString(), }; - const tokenResponse = await fetch(tokenEndpoint, tokenFetchParams); + const tokenResponse = await fetchClient(tokenEndpoint, tokenFetchParams); const tokenJson = await tokenResponse.json(); const { access_token } = tokenJson; @@ -57,7 +58,7 @@ export async function fetchFastApiKey(code: string, verification: string): Promi }), }; - const response = await fetch(fastKeyEndpoint, fastKeyFetchParams); + const response = await fetchClient(fastKeyEndpoint, fastKeyFetchParams); const fastApiKeyJson = await response.json(); const { result } = fastApiKeyJson; @@ -68,7 +69,12 @@ export async function fetchFastApiKey(code: string, verification: string): Promi } return result; } catch (error) { - console.error(error); + if (error instanceof HTTPError) { + const text = await error.response.text(); + console.error('HTTPError', error.requestId, text); + } else { + console.error(error); + } handleError(error, 'ERROR_CONTEXT_PLACEHOLDER'); throw new Error('Error fetching fast api keys'); } diff --git a/src/dAppIntegration/scripts.ts b/src/dAppIntegration/scripts.ts index ccc9a3e6..e6e08bdd 100644 --- a/src/dAppIntegration/scripts.ts +++ b/src/dAppIntegration/scripts.ts @@ -1,3 +1,5 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck import type { Platform } from 'react-native'; import type { @@ -630,7 +632,9 @@ function injectProviders(secret: string, platform: typeof Platform.OS, solanaSdk requestMap.delete(responseOrEvent.id); } - } catch {} + } catch { + return; // ignore malformed messages + } } if (platform === 'ios') { diff --git a/src/screens/Receive/hooks/useReceiveAddress.ts b/src/screens/Receive/hooks/useReceiveAddress.ts index e09c3323..4b406542 100644 --- a/src/screens/Receive/hooks/useReceiveAddress.ts +++ b/src/screens/Receive/hooks/useReceiveAddress.ts @@ -28,9 +28,10 @@ export const getReceiveAddress = async ( await timeout(transport.fetchState?.(wallet, network, storage), 3000); } catch (e) { if (e instanceof Timeout) { - } else { - await handleError(e, 'ERROR_CONTEXT_PLACEHOLDER'); + // ignore fetch timeout + return; } + await handleError(e, 'ERROR_CONTEXT_PLACEHOLDER'); } } diff --git a/src/utils/isSvgImage.ts b/src/utils/isSvgImage.ts index 2ca04201..7a572568 100644 --- a/src/utils/isSvgImage.ts +++ b/src/utils/isSvgImage.ts @@ -1,7 +1,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import { debounce } from 'lodash'; -import { fetchClient } from '@/api/base/fetchClient'; +import { fetchClient, HTTPError } from '@/api/base/fetchClient'; const NFT_IMAGE_IS_SVG: Record = {}; @@ -27,9 +27,17 @@ export async function isSvgImage(imageUrl?: string | null) { return cached; } - const result = await fetchClient(imageUrl, { - method: 'HEAD', - }); + let result: Response; + try { + result = await fetchClient(imageUrl, { + method: 'HEAD', + }); + } catch (e) { + if (e instanceof HTTPError) { + return false; + } + throw e; + } const isSvg = isContentTypeSvg(result.headers.get('content-type') ?? ''); diff --git a/tsconfig.json b/tsconfig.json index 7064ea65..49f649a4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,5 +18,11 @@ "/*": ["./*"] } }, - "exclude": ["node_modules", "babel.config.js", "metro.config.js", "jest.config.js"] + "exclude": [ + "node_modules", + "babel.config.js", + "metro.config.js", + "jest.config.js", + "src/dAppIntegration/scripts.ts" + ] }