diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f18db0..1e4dfa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased - Update CODEOWNERS - Updated `getAuthorization` to use the correct API URL. +- Rename `getConnection(name: string)` -> `getAuthorization(developerName: string, attachmentNameOrColorUrl = "HEROKU_APPLINK")`, accepting a new attachmentNameOrColorOrUrl to use a specific Applink addon's config. +- Remove node-fetch in favor of native fetch, add `HTTPResponseError` ## [0.1.0-ea] - 2024-08-12 - Initial - -## [1.0.0] - -- Rename `getConnection(name: string)` -> `getAuthorization(developerName: string, attachmentNameOrColorUrl = "HEROKU_APPLINK")`, accepting a new attachmentNameOrColorOrUrl to use a specific Applink addon's config. \ No newline at end of file diff --git a/package.json b/package.json index 9c39c10..34b39d6 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ "csv-stringify": "^6.2.3", "jsforce": "^2.0.0-beta.24", "luxon": "^3.2.1", - "node-fetch": "^2.7.0", "throng": "^5.0.0", "whatwg-mimetype": "^3.0.0" }, diff --git a/src/add-ons/heroku-applink.ts b/src/add-ons/heroku-applink.ts index 7e3453c..1ac91c3 100644 --- a/src/add-ons/heroku-applink.ts +++ b/src/add-ons/heroku-applink.ts @@ -5,7 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import { HttpRequestUtil } from "../utils/request"; +import { HttpRequestUtil, HTTPResponseError } from "../utils/request"; import { OrgImpl } from "../sdk/org"; import { Org } from "../index"; import { @@ -58,16 +58,24 @@ export async function getAuthorization( try { response = await HTTP_REQUEST.request(authUrl, opts); } catch (err) { + if (err instanceof HTTPResponseError) { + let errorResponse; + try { + errorResponse = await err.response.json(); + } catch (jsonError) { + // If JSON parsing fails, fall through to the generic error + } + + if (errorResponse?.title && errorResponse?.detail) { + throw new Error(`${errorResponse.title} - ${errorResponse.detail}`); + } + } + throw new Error( `Unable to get connection ${developerName}: ${err.message}` ); } - // error response - if (response.title && response.detail) { - throw new Error(`${response.title} - ${response.detail}`); - } - return new OrgImpl( response.org.user_auth.access_token, response.org.api_version, diff --git a/src/index.ts b/src/index.ts index a5c8abd..c21fcde 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ import { InvocationEventImpl } from "./sdk/invocation-event.js"; import { LoggerImpl } from "./sdk/logger.js"; import { QueryOperation } from "jsforce/lib/api/bulk"; import { getAuthorization } from "./add-ons/heroku-applink.js"; +import { HTTPResponseError } from "./utils/request.js"; const CONTENT_TYPE_HEADER = "Content-Type"; const X_CLIENT_CONTEXT_HEADER = "x-client-context"; @@ -105,6 +106,8 @@ export function parseDataActionEvent(payload: any): DataCloudActionEvent { return payload as DataCloudActionEvent; } +export { HTTPResponseError }; + // T Y P E S /** diff --git a/src/utils/request.ts b/src/utils/request.ts index 5625452..9509923 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -5,7 +5,14 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import fetch from "node-fetch"; +/** Error thrown by the SDK when receiving non-2xx responses on HTTP requests. */ +export class HTTPResponseError extends Error { + response: any; + constructor(response: Response) { + super(`HTTP Error Response: ${response.status}: ${response.statusText}`); + this.response = response; + } +} /** * Handles HTTP requests. @@ -13,6 +20,11 @@ import fetch from "node-fetch"; export class HttpRequestUtil { async request(url: string, opts: any, json = true) { const response = await fetch(url, opts); + + if (!response.ok) { + throw new HTTPResponseError(response); + } + return json ? response.json() : response; } } diff --git a/test/add-ons/heroku-applink.test.ts b/test/add-ons/heroku-applink.test.ts index 157f6bc..c2f7cb2 100644 --- a/test/add-ons/heroku-applink.test.ts +++ b/test/add-ons/heroku-applink.test.ts @@ -7,7 +7,7 @@ import { expect } from "chai"; import sinon from "sinon"; -import { HttpRequestUtil } from "../../src/utils/request"; +import { HttpRequestUtil, HTTPResponseError } from "../../src/utils/request"; import { getAuthorization } from "../../src/add-ons/heroku-applink"; import { OrgImpl } from "../../src/sdk/org"; @@ -110,10 +110,14 @@ describe("getAuthorization", () => { }); it("should throw error when response contains error details", async () => { - httpRequestStub.resolves({ - title: "Not Found", - detail: "Authorization not found", - }); + const errorResponse = new Response( + JSON.stringify({ + title: "Not Found", + detail: "Authorization not found", + }), + { status: 404 } + ); + httpRequestStub.rejects(new HTTPResponseError(errorResponse)); try { await getAuthorization("testDev"); @@ -122,4 +126,20 @@ describe("getAuthorization", () => { expect(error.message).to.equal("Not Found - Authorization not found"); } }); + + it("should handle non-JSON error responses gracefully", async () => { + const invalidJsonResponse = new Response("Invalid JSON content", { + status: 500, + }); + httpRequestStub.rejects(new HTTPResponseError(invalidJsonResponse)); + + try { + await getAuthorization("testDev"); + expect.fail("Should have thrown an error"); + } catch (error) { + expect(error.message).to.equal( + "Unable to get connection testDev: HTTP Error Response: 500: " + ); + } + }); }); diff --git a/yarn.lock b/yarn.lock index 7487f22..35df472 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2648,7 +2648,7 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" -node-fetch@^2.6.1, node-fetch@^2.7.0: +node-fetch@^2.6.1: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==