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