1import { getExpoHomeDirectory } from '@expo/config/build/getUserState'; 2import { JSONValue } from '@expo/json-file'; 3import fetchInstance from 'node-fetch'; 4import path from 'path'; 5 6import { env } from '../../utils/env'; 7import { getExpoApiBaseUrl } from '../endpoint'; 8import UserSettings from '../user/UserSettings'; 9import { FileSystemCache } from './cache/FileSystemCache'; 10import { wrapFetchWithCache } from './cache/wrapFetchWithCache'; 11import { FetchLike } from './client.types'; 12import { wrapFetchWithBaseUrl } from './wrapFetchWithBaseUrl'; 13import { wrapFetchWithOffline } from './wrapFetchWithOffline'; 14import { wrapFetchWithProgress } from './wrapFetchWithProgress'; 15import { wrapFetchWithProxy } from './wrapFetchWithProxy'; 16 17export class ApiV2Error extends Error { 18 readonly name = 'ApiV2Error'; 19 readonly code: string; 20 readonly expoApiV2ErrorCode: string; 21 readonly expoApiV2ErrorDetails?: JSONValue; 22 readonly expoApiV2ErrorServerStack?: string; 23 readonly expoApiV2ErrorMetadata?: object; 24 25 constructor(response: { 26 message: string; 27 code: string; 28 stack?: string; 29 details?: JSONValue; 30 metadata?: object; 31 }) { 32 super(response.message); 33 this.code = response.code; 34 this.expoApiV2ErrorCode = response.code; 35 this.expoApiV2ErrorDetails = response.details; 36 this.expoApiV2ErrorServerStack = response.stack; 37 this.expoApiV2ErrorMetadata = response.metadata; 38 } 39} 40 41/** 42 * An Expo server error that didn't return the expected error JSON information. 43 * The only 'expected' place for this is in testing, all other cases are bugs with the server. 44 */ 45export class UnexpectedServerError extends Error { 46 readonly name = 'UnexpectedServerError'; 47} 48 49/** 50 * @returns a `fetch` function that will inject user authentication information and handle errors from the Expo API. 51 */ 52export function wrapFetchWithCredentials(fetchFunction: FetchLike): FetchLike { 53 return async function fetchWithCredentials(url, options = {}) { 54 if (Array.isArray(options.headers)) { 55 throw new Error('request headers must be in object form'); 56 } 57 58 const resolvedHeaders = options.headers ?? ({} as any); 59 60 const token = UserSettings.getAccessToken(); 61 if (token) { 62 resolvedHeaders.authorization = `Bearer ${token}`; 63 } else { 64 const sessionSecret = UserSettings.getSession()?.sessionSecret; 65 if (sessionSecret) { 66 resolvedHeaders['expo-session'] = sessionSecret; 67 } 68 } 69 70 const results = await fetchFunction(url, { 71 ...options, 72 headers: resolvedHeaders, 73 }); 74 75 if (results.status >= 400 && results.status < 500) { 76 const body = await results.text(); 77 try { 78 const data = JSON.parse(body); 79 if (data?.errors?.length) { 80 throw new ApiV2Error(data.errors[0]); 81 } 82 } catch (error: any) { 83 // Server returned non-json response. 84 if (error.message.includes('in JSON at position')) { 85 throw new UnexpectedServerError(body); 86 } 87 throw error; 88 } 89 } 90 return results; 91 }; 92} 93 94const fetchWithOffline = wrapFetchWithOffline(fetchInstance); 95 96const fetchWithBaseUrl = wrapFetchWithBaseUrl(fetchWithOffline, getExpoApiBaseUrl() + '/v2/'); 97 98const fetchWithProxy = wrapFetchWithProxy(fetchWithBaseUrl); 99 100const fetchWithCredentials = wrapFetchWithProgress(wrapFetchWithCredentials(fetchWithProxy)); 101 102/** 103 * Create an instance of the fully qualified fetch command (auto authentication and api) but with caching in the '~/.expo' directory. 104 * Caching is disabled automatically if the EXPO_NO_CACHE or EXPO_BETA environment variables are enabled. 105 */ 106export function createCachedFetch({ 107 fetch = fetchWithCredentials, 108 cacheDirectory, 109 ttl, 110 skipCache, 111}: { 112 fetch?: FetchLike; 113 cacheDirectory: string; 114 ttl?: number; 115 skipCache?: boolean; 116}): FetchLike { 117 // Disable all caching in EXPO_BETA. 118 if (skipCache || env.EXPO_BETA || env.EXPO_NO_CACHE) { 119 return fetch; 120 } 121 122 return wrapFetchWithCache( 123 fetch, 124 new FileSystemCache({ 125 cacheDirectory: path.join(getExpoHomeDirectory(), cacheDirectory), 126 ttl, 127 }) 128 ); 129} 130 131/** Instance of fetch with automatic base URL pointing to the Expo API, user credential injection, and API error handling. Caching not included. */ 132export const fetchAsync = wrapFetchWithProgress(wrapFetchWithCredentials(fetchWithProxy)); 133