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