From 74de58c7300d8b6f93f495830dd245248d192a4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allen=20Jim=C3=A9nez?= <107066142+ajimenezlyft@users.noreply.github.com> Date: Fri, 21 Jun 2024 11:42:23 -0600 Subject: [PATCH] network: Create `proxyClient` to handle errors on API calls done via the proxy API (#3046) ### Description API calls done via the proxy API always return 200 unless there's a 5XX error. There's a `httpStatus` property that's returned inside the `data` property on Axios calls which contains the actual HTTP status. The new `proxyClient` now will be able to handle those errors correctly. ### Testing Performed Unit testing and tested on internal UI. --- frontend/packages/core/src/Network/errors.ts | 21 ++++++++- frontend/packages/core/src/Network/index.ts | 45 ++++++++++++++++--- .../core/src/Network/tests/index.test.ts | 33 +++++++++++++- frontend/packages/core/src/index.tsx | 2 +- 4 files changed, 91 insertions(+), 10 deletions(-) diff --git a/frontend/packages/core/src/Network/errors.ts b/frontend/packages/core/src/Network/errors.ts index 00abd93f7f..b07413cb8e 100644 --- a/frontend/packages/core/src/Network/errors.ts +++ b/frontend/packages/core/src/Network/errors.ts @@ -281,6 +281,25 @@ const grpcResponseToError = (clientError: AxiosError): ClutchError => { return error; }; +const HTTP_CODE_MAPPING = { + 200: "OK", + 400: "Failed Precondition", + 401: "Unauthenticated", + 403: "Permission Denied", + 404: "Not Found", + 409: "Already Exists", + 429: "Resource Exhausted", + 499: "Cancelled", + 500: "Internal Server Error", + 501: "Not Implemented", + 503: "Service Unavailable", + 504: "Gateway Timeout", +}; + +const httpCodeToText = (code: number): string => { + return HTTP_CODE_MAPPING[code] || "Unknown"; +}; + /* eslint-disable no-underscore-dangle */ const isHelpDetails = (details: ErrorDetails): details is Help => { return details._type === "types.googleapis.com/google.rpc.Help"; @@ -291,4 +310,4 @@ const isClutchErrorDetails = (details: ErrorDetails): details is IClutch.api.v1. }; /* eslint-enable */ -export { grpcResponseToError, isClutchErrorDetails, isHelpDetails }; +export { grpcResponseToError, httpCodeToText, isClutchErrorDetails, isHelpDetails }; diff --git a/frontend/packages/core/src/Network/index.ts b/frontend/packages/core/src/Network/index.ts index d0c70b7271..ded13906fb 100644 --- a/frontend/packages/core/src/Network/index.ts +++ b/frontend/packages/core/src/Network/index.ts @@ -2,7 +2,7 @@ import type { AxiosError, AxiosResponse } from "axios"; import axios from "axios"; import type { ClutchError } from "./errors"; -import { grpcResponseToError } from "./errors"; +import { grpcResponseToError, httpCodeToText } from "./errors"; /** * HTTP response status. @@ -21,10 +21,34 @@ export interface HttpStatus { text: string; } +interface CreateClientProps { + response?: { + success: (response: AxiosResponse) => AxiosResponse | Promise; + error: (error: AxiosError) => Promise; + }; +} + const successInterceptor = (response: AxiosResponse) => { return response; }; +const successProxyInterceptor = (response: AxiosResponse) => { + // Handle proxy errors + if (response.data.httpStatus >= 400) { + const error = { + status: { + code: response.data.httpStatus, + text: httpCodeToText(response.data.httpStatus), + }, + message: response.data.response.message, + data: response.data, + } as ClutchError; + return Promise.reject(error); + } + + return response; +}; + const errorInterceptor = (error: AxiosError): Promise => { const response = error?.response; if (response === undefined) { @@ -49,7 +73,7 @@ const errorInterceptor = (error: AxiosError): Promise => { // since we have already accounted for axios errors. const responseData = error?.response?.data; // if the response data has a code on it we know it's a gRPC response. - let err; + let err: ClutchError; if (responseData?.code !== undefined) { err = grpcResponseToError(error); } else { @@ -69,18 +93,27 @@ const errorInterceptor = (error: AxiosError): Promise => { return Promise.reject(err); }; -const createClient = () => { +const createClient = ({ response }: CreateClientProps) => { const axiosClient = axios.create({ // n.b. the client will treat any response code >= 400 as an error and apply the error interceptor. validateStatus: status => { return status < 400; }, }); - axiosClient.interceptors.response.use(successInterceptor, errorInterceptor); + + if (response) { + axiosClient.interceptors.response.use(response.success, response.error); + } return axiosClient; }; -const client = createClient(); +const client = createClient({ + response: { success: successInterceptor, error: errorInterceptor }, +}); + +const proxyClient = createClient({ + response: { success: successProxyInterceptor, error: errorInterceptor }, +}); -export { client, errorInterceptor, successInterceptor }; +export { client, proxyClient, errorInterceptor, successInterceptor, successProxyInterceptor }; diff --git a/frontend/packages/core/src/Network/tests/index.test.ts b/frontend/packages/core/src/Network/tests/index.test.ts index fac2c1c101..bbf43ce625 100644 --- a/frontend/packages/core/src/Network/tests/index.test.ts +++ b/frontend/packages/core/src/Network/tests/index.test.ts @@ -1,7 +1,7 @@ -import type { AxiosError } from "axios"; +import type { AxiosError, AxiosResponse } from "axios"; import type { ClutchError } from "../errors"; -import { client, errorInterceptor } from "../index"; +import { client, errorInterceptor, successProxyInterceptor } from "../index"; describe("error interceptor", () => { describe("on axios error", () => { @@ -151,3 +151,32 @@ describe("axios client", () => { expect(client.defaults.validateStatus(399)).toBe(true); }); }); + +describe("axios success proxy interceptor", () => { + it("returns an error if the proxy call returns response.data.httpStatus >= 400", async () => { + const response = { + status: 200, + statusText: "Not Found", + data: { + httpStatus: 404, + headers: { + "Cache-Control": ["no-cache"], + }, + response: { + message: "Item not found", + }, + }, + } as AxiosResponse; + + const err = { + status: { + code: 404, + text: "Not Found", + }, + message: "Item not found", + data: response.data, + } as ClutchError; + + await expect(successProxyInterceptor(response)).rejects.toEqual(err); + }); +}); diff --git a/frontend/packages/core/src/index.tsx b/frontend/packages/core/src/index.tsx index 66a6be3013..3dbb19375f 100644 --- a/frontend/packages/core/src/index.tsx +++ b/frontend/packages/core/src/index.tsx @@ -35,7 +35,7 @@ export { useParams, useSearchParams, } from "./navigation"; -export { client } from "./Network"; +export { client, proxyClient } from "./Network"; export * from "./NPS"; export { default as ExpansionPanel } from "./panel"; export { default as Paper } from "./paper";