xref: /expo/packages/@expo/cli/src/api/rest/client.ts (revision 8a424beb)
18d307f52SEvan Baconimport { getExpoHomeDirectory } from '@expo/config/build/getUserState';
28d307f52SEvan Baconimport { JSONValue } from '@expo/json-file';
38d307f52SEvan Baconimport fetchInstance from 'node-fetch';
48d307f52SEvan Baconimport path from 'path';
58d307f52SEvan Bacon
68d307f52SEvan Baconimport { FileSystemCache } from './cache/FileSystemCache';
78d307f52SEvan Baconimport { wrapFetchWithCache } from './cache/wrapFetchWithCache';
88d307f52SEvan Baconimport { FetchLike } from './client.types';
98d307f52SEvan Baconimport { wrapFetchWithBaseUrl } from './wrapFetchWithBaseUrl';
108d307f52SEvan Baconimport { wrapFetchWithOffline } from './wrapFetchWithOffline';
1182f3de79SEvan Baconimport { wrapFetchWithProgress } from './wrapFetchWithProgress';
128c8eefe0SEvan Baconimport { wrapFetchWithProxy } from './wrapFetchWithProxy';
13*8a424bebSJames Ideimport { env } from '../../utils/env';
14*8a424bebSJames Ideimport { CommandError } from '../../utils/errors';
15*8a424bebSJames Ideimport { getExpoApiBaseUrl } from '../endpoint';
16*8a424bebSJames Ideimport { disableNetwork } from '../settings';
17*8a424bebSJames Ideimport UserSettings from '../user/UserSettings';
188d307f52SEvan Bacon
198d307f52SEvan Baconexport class ApiV2Error extends Error {
208d307f52SEvan Bacon  readonly name = 'ApiV2Error';
215529feceSEvan Bacon  readonly code: string;
228d307f52SEvan Bacon  readonly expoApiV2ErrorCode: string;
238d307f52SEvan Bacon  readonly expoApiV2ErrorDetails?: JSONValue;
248d307f52SEvan Bacon  readonly expoApiV2ErrorServerStack?: string;
258d307f52SEvan Bacon  readonly expoApiV2ErrorMetadata?: object;
268d307f52SEvan Bacon
278d307f52SEvan Bacon  constructor(response: {
288d307f52SEvan Bacon    message: string;
298d307f52SEvan Bacon    code: string;
308d307f52SEvan Bacon    stack?: string;
318d307f52SEvan Bacon    details?: JSONValue;
328d307f52SEvan Bacon    metadata?: object;
338d307f52SEvan Bacon  }) {
348d307f52SEvan Bacon    super(response.message);
355529feceSEvan Bacon    this.code = response.code;
368d307f52SEvan Bacon    this.expoApiV2ErrorCode = response.code;
378d307f52SEvan Bacon    this.expoApiV2ErrorDetails = response.details;
388d307f52SEvan Bacon    this.expoApiV2ErrorServerStack = response.stack;
398d307f52SEvan Bacon    this.expoApiV2ErrorMetadata = response.metadata;
408d307f52SEvan Bacon  }
418d307f52SEvan Bacon}
428d307f52SEvan Bacon
438d307f52SEvan Bacon/**
448d307f52SEvan Bacon * An Expo server error that didn't return the expected error JSON information.
458d307f52SEvan Bacon * The only 'expected' place for this is in testing, all other cases are bugs with the server.
468d307f52SEvan Bacon */
478d307f52SEvan Baconexport class UnexpectedServerError extends Error {
488d307f52SEvan Bacon  readonly name = 'UnexpectedServerError';
498d307f52SEvan Bacon}
508d307f52SEvan Bacon
518d307f52SEvan Bacon/**
528d307f52SEvan Bacon * @returns a `fetch` function that will inject user authentication information and handle errors from the Expo API.
538d307f52SEvan Bacon */
548d307f52SEvan Baconexport function wrapFetchWithCredentials(fetchFunction: FetchLike): FetchLike {
558d307f52SEvan Bacon  return async function fetchWithCredentials(url, options = {}) {
568d307f52SEvan Bacon    if (Array.isArray(options.headers)) {
578d307f52SEvan Bacon      throw new Error('request headers must be in object form');
588d307f52SEvan Bacon    }
598d307f52SEvan Bacon
608d307f52SEvan Bacon    const resolvedHeaders = options.headers ?? ({} as any);
618d307f52SEvan Bacon
628d307f52SEvan Bacon    const token = UserSettings.getAccessToken();
638d307f52SEvan Bacon    if (token) {
648d307f52SEvan Bacon      resolvedHeaders.authorization = `Bearer ${token}`;
658d307f52SEvan Bacon    } else {
668d307f52SEvan Bacon      const sessionSecret = UserSettings.getSession()?.sessionSecret;
678d307f52SEvan Bacon      if (sessionSecret) {
688d307f52SEvan Bacon        resolvedHeaders['expo-session'] = sessionSecret;
698d307f52SEvan Bacon      }
708d307f52SEvan Bacon    }
718d307f52SEvan Bacon
72e32ccf9fSEvan Bacon    try {
738d307f52SEvan Bacon      const results = await fetchFunction(url, {
748d307f52SEvan Bacon        ...options,
758d307f52SEvan Bacon        headers: resolvedHeaders,
768d307f52SEvan Bacon      });
778d307f52SEvan Bacon
788d307f52SEvan Bacon      if (results.status >= 400 && results.status < 500) {
798d307f52SEvan Bacon        const body = await results.text();
808d307f52SEvan Bacon        try {
818d307f52SEvan Bacon          const data = JSON.parse(body);
828d307f52SEvan Bacon          if (data?.errors?.length) {
838d307f52SEvan Bacon            throw new ApiV2Error(data.errors[0]);
848d307f52SEvan Bacon          }
8529975bfdSEvan Bacon        } catch (error: any) {
868d307f52SEvan Bacon          // Server returned non-json response.
878d307f52SEvan Bacon          if (error.message.includes('in JSON at position')) {
888d307f52SEvan Bacon            throw new UnexpectedServerError(body);
898d307f52SEvan Bacon          }
908d307f52SEvan Bacon          throw error;
918d307f52SEvan Bacon        }
928d307f52SEvan Bacon      }
938d307f52SEvan Bacon      return results;
94e32ccf9fSEvan Bacon    } catch (error: any) {
95e32ccf9fSEvan Bacon      // Specifically, when running `npx expo start` and the wifi is connected but not really (public wifi, airplanes, etc).
96e32ccf9fSEvan Bacon      if ('code' in error && error.code === 'ENOTFOUND') {
97e32ccf9fSEvan Bacon        disableNetwork();
98e32ccf9fSEvan Bacon
99e32ccf9fSEvan Bacon        throw new CommandError(
100e32ccf9fSEvan Bacon          'OFFLINE',
101e32ccf9fSEvan Bacon          'Network connection is unreliable. Try again with the environment variable `EXPO_OFFLINE=1` to skip network requests.'
102e32ccf9fSEvan Bacon        );
103e32ccf9fSEvan Bacon      }
104e32ccf9fSEvan Bacon
105e32ccf9fSEvan Bacon      throw error;
106e32ccf9fSEvan Bacon    }
1078d307f52SEvan Bacon  };
1088d307f52SEvan Bacon}
1098d307f52SEvan Bacon
1108d307f52SEvan Baconconst fetchWithOffline = wrapFetchWithOffline(fetchInstance);
1118d307f52SEvan Bacon
1128d307f52SEvan Baconconst fetchWithBaseUrl = wrapFetchWithBaseUrl(fetchWithOffline, getExpoApiBaseUrl() + '/v2/');
1138d307f52SEvan Bacon
1148c8eefe0SEvan Baconconst fetchWithProxy = wrapFetchWithProxy(fetchWithBaseUrl);
1158c8eefe0SEvan Bacon
11682f3de79SEvan Baconconst fetchWithCredentials = wrapFetchWithProgress(wrapFetchWithCredentials(fetchWithProxy));
1178d307f52SEvan Bacon
1188d307f52SEvan Bacon/**
1198d307f52SEvan Bacon * Create an instance of the fully qualified fetch command (auto authentication and api) but with caching in the '~/.expo' directory.
1208d307f52SEvan Bacon * Caching is disabled automatically if the EXPO_NO_CACHE or EXPO_BETA environment variables are enabled.
1218d307f52SEvan Bacon */
1228d307f52SEvan Baconexport function createCachedFetch({
12382f3de79SEvan Bacon  fetch = fetchWithCredentials,
1248d307f52SEvan Bacon  cacheDirectory,
1258d307f52SEvan Bacon  ttl,
12609bb6093SEvan Bacon  skipCache,
1278d307f52SEvan Bacon}: {
1288d307f52SEvan Bacon  fetch?: FetchLike;
1298d307f52SEvan Bacon  cacheDirectory: string;
1308d307f52SEvan Bacon  ttl?: number;
13109bb6093SEvan Bacon  skipCache?: boolean;
1328d307f52SEvan Bacon}): FetchLike {
1338d307f52SEvan Bacon  // Disable all caching in EXPO_BETA.
134814b6fafSEvan Bacon  if (skipCache || env.EXPO_BETA || env.EXPO_NO_CACHE) {
13582f3de79SEvan Bacon    return fetch;
1368d307f52SEvan Bacon  }
1378d307f52SEvan Bacon
1388d307f52SEvan Bacon  return wrapFetchWithCache(
13982f3de79SEvan Bacon    fetch,
1408d307f52SEvan Bacon    new FileSystemCache({
1418d307f52SEvan Bacon      cacheDirectory: path.join(getExpoHomeDirectory(), cacheDirectory),
1428d307f52SEvan Bacon      ttl,
1438d307f52SEvan Bacon    })
1448d307f52SEvan Bacon  );
1458d307f52SEvan Bacon}
1468d307f52SEvan Bacon
1478d307f52SEvan Bacon/** Instance of fetch with automatic base URL pointing to the Expo API, user credential injection, and API error handling. Caching not included.  */
14882f3de79SEvan Baconexport const fetchAsync = wrapFetchWithProgress(wrapFetchWithCredentials(fetchWithProxy));
149