From d1c30dc35ff96048af63b7eb0573494091ed827e Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Tue, 21 Nov 2023 12:57:49 +0100 Subject: [PATCH] Throw error if cloud apl GET fails (#296) --- .changeset/sixty-geese-whisper.md | 5 ++ src/APL/index.ts | 2 +- src/APL/saleor-cloud/index.ts | 2 + .../saleor-cloud/saleor-cloud-apl-errors.ts | 6 +++ .../saleor-cloud-apl.test.ts | 49 +++++++++++++++++- .../{ => saleor-cloud}/saleor-cloud-apl.ts | 50 ++++++++++++------- 6 files changed, 95 insertions(+), 19 deletions(-) create mode 100644 .changeset/sixty-geese-whisper.md create mode 100644 src/APL/saleor-cloud/index.ts create mode 100644 src/APL/saleor-cloud/saleor-cloud-apl-errors.ts rename src/APL/{ => saleor-cloud}/saleor-cloud-apl.test.ts (75%) rename src/APL/{ => saleor-cloud}/saleor-cloud-apl.ts (84%) diff --git a/.changeset/sixty-geese-whisper.md b/.changeset/sixty-geese-whisper.md new file mode 100644 index 00000000..6902d13c --- /dev/null +++ b/.changeset/sixty-geese-whisper.md @@ -0,0 +1,5 @@ +--- +"@saleor/app-sdk": minor +--- + +Changed behavior or Saleor Cloud APL - get method. Prevoiusly ANY error was catched and method returned "undefined". Now only 404-like errors will return "undefined" and error like 5xx, timeouts, wrong body, etc. will result with thrown error. This is technically a breaking change on function level, but in the end app will fail anyway - if APL is not found, it can't proceed. Now error is thrown earlier. It should help with debugging and better custom error handling. diff --git a/src/APL/index.ts b/src/APL/index.ts index 6441263a..1924e22d 100644 --- a/src/APL/index.ts +++ b/src/APL/index.ts @@ -1,5 +1,5 @@ export * from "./apl"; export * from "./env-apl"; export * from "./file-apl"; -export * from "./saleor-cloud-apl"; +export * from "./saleor-cloud"; export * from "./upstash-apl"; diff --git a/src/APL/saleor-cloud/index.ts b/src/APL/saleor-cloud/index.ts new file mode 100644 index 00000000..19c4007e --- /dev/null +++ b/src/APL/saleor-cloud/index.ts @@ -0,0 +1,2 @@ +export * from "./saleor-cloud-apl"; +export * from "./saleor-cloud-apl-errors"; diff --git a/src/APL/saleor-cloud/saleor-cloud-apl-errors.ts b/src/APL/saleor-cloud/saleor-cloud-apl-errors.ts new file mode 100644 index 00000000..e26e0b7e --- /dev/null +++ b/src/APL/saleor-cloud/saleor-cloud-apl-errors.ts @@ -0,0 +1,6 @@ +export class SaleorCloudAplError extends Error { + constructor(public code: string, message: string) { + super(message); + this.name = "SaleorCloudAplError"; + } +} diff --git a/src/APL/saleor-cloud-apl.test.ts b/src/APL/saleor-cloud/saleor-cloud-apl.test.ts similarity index 75% rename from src/APL/saleor-cloud-apl.test.ts rename to src/APL/saleor-cloud/saleor-cloud-apl.test.ts index 29b51fa9..d97f5d09 100644 --- a/src/APL/saleor-cloud-apl.test.ts +++ b/src/APL/saleor-cloud/saleor-cloud-apl.test.ts @@ -1,7 +1,8 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { AuthData } from "./apl"; +import { AuthData } from "../apl"; import { GetAllAplResponseShape, SaleorCloudAPL, SaleorCloudAPLConfig } from "./saleor-cloud-apl"; +import { SaleorCloudAplError } from "./saleor-cloud-apl-errors"; const fetchMock = vi.fn(); @@ -73,6 +74,52 @@ describe("APL", () => { }); describe("get", () => { + describe("Handles failures on API level", () => { + it("Throws error if status is 500", async () => { + fetchMock.mockResolvedValueOnce({ + status: 500, + ok: false, + async json() { + throw new Error(); + }, + }); + + const apl = new SaleorCloudAPL(aplConfig); + + try { + await apl.get(stubAuthData.saleorApiUrl); + } catch (e) { + const err = e as SaleorCloudAplError; + + expect(err.code).toEqual("FAILED_TO_REACH_API"); + expect(err).toMatchInlineSnapshot("[SaleorCloudAplError: Api responded with 500]"); + } + }); + + it("Throws error if status is 200 but JSON is malformed", async () => { + fetchMock.mockResolvedValueOnce({ + status: 200, + ok: true, + async json() { + throw new Error("json error"); + }, + }); + + const apl = new SaleorCloudAPL(aplConfig); + + try { + await apl.get(stubAuthData.saleorApiUrl); + } catch (e) { + const err = e as SaleorCloudAplError; + + expect(err.code).toEqual("RESPONSE_BODY_INVALID"); + expect(err).toMatchInlineSnapshot( + "[SaleorCloudAplError: Cant parse response body: json error]" + ); + } + }); + }); + describe("Read existing auth data", () => { it("Read existing auth data", async () => { fetchMock.mockResolvedValue({ diff --git a/src/APL/saleor-cloud-apl.ts b/src/APL/saleor-cloud/saleor-cloud-apl.ts similarity index 84% rename from src/APL/saleor-cloud-apl.ts rename to src/APL/saleor-cloud/saleor-cloud-apl.ts index 837ae41a..b74c2e1d 100644 --- a/src/APL/saleor-cloud-apl.ts +++ b/src/APL/saleor-cloud/saleor-cloud-apl.ts @@ -1,7 +1,8 @@ -import { hasProp } from "../has-prop"; -import { APL, AplConfiguredResult, AplReadyResult, AuthData } from "./apl"; -import { createAPLDebug } from "./apl-debug"; -import { authDataFromObject } from "./auth-data-from-object"; +import { hasProp } from "../../has-prop"; +import { APL, AplConfiguredResult, AplReadyResult, AuthData } from "../apl"; +import { createAPLDebug } from "../apl-debug"; +import { authDataFromObject } from "../auth-data-from-object"; +import { SaleorCloudAplError } from "./saleor-cloud-apl-errors"; const debug = createAPLDebug("SaleorCloudAPL"); @@ -23,13 +24,12 @@ export type GetAllAplResponseShape = { results: CloudAPLAuthDataShape[]; }; -const validateResponseStatus = (response: Response) => { - if (response.status === 404) { - debug("Auth data not found"); - debug("%O", response); +export const CloudAplError = { + FAILED_TO_REACH_API: "FAILED_TO_REACH_API", + RESPONSE_BODY_INVALID: "RESPONSE_BODY_INVALID", +}; - throw new Error("Auth data not found"); - } +const validateResponseStatus = (response: Response) => { if (!response.ok) { debug("Response failed with status %s", response.status); debug("%O", response); @@ -102,25 +102,41 @@ export class SaleorCloudAPL implements APL { debug("Failed to reach API call: %s", extractErrorMessage(error)); debug("%O", error); - return undefined; + throw new SaleorCloudAplError( + CloudAplError.FAILED_TO_REACH_API, + `${extractErrorMessage(error)}` + ); }); if (!response) { debug("No response from the API"); - return undefined; + + throw new SaleorCloudAplError( + CloudAplError.FAILED_TO_REACH_API, + "Response couldnt be resolved" + ); } - try { - validateResponseStatus(response); - debug("Response status valid"); - } catch { - debug("Response status not valid"); + if (response.status >= 500) { + throw new SaleorCloudAplError( + CloudAplError.FAILED_TO_REACH_API, + `Api responded with ${response.status}` + ); + } + + if (response.status === 404) { + debug("No auth data for given saleorApiUrl"); return undefined; } const parsedResponse = (await response.json().catch((e) => { debug("Failed to parse response: %s", extractErrorMessage(e)); debug("%O", e); + + throw new SaleorCloudAplError( + CloudAplError.RESPONSE_BODY_INVALID, + `Cant parse response body: ${extractErrorMessage(e)}` + ); })) as CloudAPLAuthDataShape; const authData = authDataFromObject(mapAPIResponseToAuthData(parsedResponse));