All files / client/src/utils api.ts

97.5% Statements 39/40
100% Branches 12/12
100% Functions 6/6
97.5% Lines 39/40

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144                                                      521x   521x   521x 65x   65x         65x     521x         3276x   3276x       3276x 3276x 2479x       3276x 3276x           52x     3197x           3197x   27x 27x       28x   28x       3170x 16x 12x     4x   4x         4x     3154x 3154x 3141x   1x           521x           872x               2395x 2395x 463x 463x   2395x               9x      
import { config } from "shared/config";
import {
  ApiDevEndpoint,
  ApiEndpoint,
  AuthEndpoint,
} from "shared/constants/apiEndpoints";
import { ApiError } from "shared/types/api/errors";
import { fetchWithTimeout } from "client/utils/fetchWithTimeout";
import { getJWT } from "client/utils/getJWT";
import {
  isBackgroundRequest,
  onRequestFailure,
  onRequestSuccess,
  shouldTreatHttpErrorAsNetworkError,
  showApiError,
} from "client/utils/networkErrorPolicy";
 
enum HttpMethod {
  GET = "GET",
  POST = "POST",
  DELETE = "DELETE",
}
 
interface ApiResponse<T> {
  data: T;
}
 
const baseURL = config.client().apiServerUrl;
 
const REQUEST_TIMEOUT_MS = 60000;
 
const networkErrorResponse = <T>(background: boolean): ApiResponse<T> => {
  onRequestFailure(background);
 
  const error: ApiError = {
    errorId: "unknown",
    message: "Network error",
    status: "error",
  };
  return { data: error as T };
};
 
const apiFetch = async <T>(
  url: string,
  method: HttpMethod,
  body?: unknown,
): Promise<ApiResponse<T>> => {
  const background = isBackgroundRequest(method, url);
 
  const headers: Record<string, string> = {
    "Content-Type": "application/json",
  };
 
  const authToken = getJWT();
  if (authToken) {
    headers.Authorization = `Bearer ${authToken}`;
  }
 
  let response: Response;
  try {
    response = await fetchWithTimeout(`${baseURL}${url}`, REQUEST_TIMEOUT_MS, {
      method,
      headers,
      body: body === undefined ? undefined : JSON.stringify(body),
    });
  } catch {
    return networkErrorResponse<T>(background);
  }
 
  onRequestSuccess();
 
  // Reading the response body can reject with AbortError if the page unloads
  // mid-response, so json() calls need the same error handling as fetch()
 
  // Handle redirect responses (301/302 with JSON body containing location)
  if ([301, 302].includes(response.status)) {
    let data: { location: string };
    try {
      data = (await response.json()) as { location: string };
    } catch {
      return networkErrorResponse<T>(background);
    }
    location.href = data.location;
    // eslint-disable-next-line @typescript-eslint/no-empty-function
    return new Promise<ApiResponse<T>>(() => {});
  }
 
  // Handle errors
  if (!response.ok) {
    if (shouldTreatHttpErrorAsNetworkError(background)) {
      return networkErrorResponse<T>(background);
    }
 
    showApiError(method, url, response.status);
 
    const error: ApiError = {
      errorId: "unknown",
      message: "Invalid API response",
      status: "error",
    };
    return { data: error as T };
  }
 
  try {
    const data = (await response.json()) as T;
    return { data };
  } catch {
    return networkErrorResponse<T>(background);
  }
};
 
type Endpoint = ApiEndpoint | ApiDevEndpoint | AuthEndpoint;
 
export const api = {
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
  post: async <RES = never, REQ = never>(
    url: Endpoint,
    data?: REQ,
  ): Promise<ApiResponse<RES>> => {
    return await apiFetch<RES>(url, HttpMethod.POST, data);
  },
 
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
  get: async <RES = never, REQ = never>(
    url: Endpoint,
    options?: { params?: REQ },
  ): Promise<ApiResponse<RES>> => {
    let fetchUrl: string = url;
    if (options?.params) {
      const searchParams = new URLSearchParams(options.params);
      fetchUrl = `${url}?${searchParams.toString()}`;
    }
    return await apiFetch<RES>(fetchUrl, HttpMethod.GET);
  },
 
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
  delete: async <RES = never, REQ = never>(
    url: Endpoint,
    options?: { data?: REQ },
  ): Promise<ApiResponse<RES>> => {
    return await apiFetch<RES>(url, HttpMethod.DELETE, options?.data);
  },
};