From cbe36bd237d165b9423c7b24aa5e4e7ee6bd0445 Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 4 Jun 2025 12:01:13 -0500 Subject: [PATCH 1/2] use native fetch, throw for non-2xxs --- CHANGELOG.md | 6 ++---- package.json | 1 - src/add-ons/heroku-applink.ts | 20 ++++++++++++++----- src/index.ts | 4 ++++ src/utils/request.ts | 15 +++++++++++++- test/add-ons/heroku-applink.test.ts | 31 ++++++++++++++++++++++++----- yarn.lock | 2 +- 7 files changed, 62 insertions(+), 17 deletions(-) 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..b3fd3b1 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,15 +58,25 @@ 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, diff --git a/src/index.ts b/src/index.ts index a5c8abd..1d0e4cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,10 +10,12 @@ 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"; + // F U N C T I O N S /** @@ -105,6 +107,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..1d9b73f 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -5,7 +5,15 @@ * 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 +21,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..2aaf9b2 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,21 @@ 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== From 79f30b086dd223fb3554030d0e930ddf25244e3d Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 4 Jun 2025 13:57:53 -0500 Subject: [PATCH 2/2] format --- src/add-ons/heroku-applink.ts | 4 +--- src/index.ts | 1 - src/utils/request.ts | 9 ++++----- test/add-ons/heroku-applink.test.ts | 9 ++++----- 4 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/add-ons/heroku-applink.ts b/src/add-ons/heroku-applink.ts index b3fd3b1..1ac91c3 100644 --- a/src/add-ons/heroku-applink.ts +++ b/src/add-ons/heroku-applink.ts @@ -70,14 +70,12 @@ export async function getAuthorization( throw new Error(`${errorResponse.title} - ${errorResponse.detail}`); } } - + throw new Error( `Unable to get connection ${developerName}: ${err.message}` ); } - - return new OrgImpl( response.org.user_auth.access_token, response.org.api_version, diff --git a/src/index.ts b/src/index.ts index 1d0e4cf..c21fcde 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,7 +15,6 @@ import { HTTPResponseError } from "./utils/request.js"; const CONTENT_TYPE_HEADER = "Content-Type"; const X_CLIENT_CONTEXT_HEADER = "x-client-context"; - // F U N C T I O N S /** diff --git a/src/utils/request.ts b/src/utils/request.ts index 1d9b73f..9509923 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -8,13 +8,12 @@ /** 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; - } + constructor(response: Response) { + super(`HTTP Error Response: ${response.status}: ${response.statusText}`); + this.response = response; + } } - /** * Handles HTTP requests. */ diff --git a/test/add-ons/heroku-applink.test.ts b/test/add-ons/heroku-applink.test.ts index 2aaf9b2..c2f7cb2 100644 --- a/test/add-ons/heroku-applink.test.ts +++ b/test/add-ons/heroku-applink.test.ts @@ -113,7 +113,7 @@ describe("getAuthorization", () => { const errorResponse = new Response( JSON.stringify({ title: "Not Found", - detail: "Authorization not found" + detail: "Authorization not found", }), { status: 404 } ); @@ -128,10 +128,9 @@ describe("getAuthorization", () => { }); it("should handle non-JSON error responses gracefully", async () => { - const invalidJsonResponse = new Response( - "Invalid JSON content", - { status: 500 } - ); + const invalidJsonResponse = new Response("Invalid JSON content", { + status: 500, + }); httpRequestStub.rejects(new HTTPResponseError(invalidJsonResponse)); try {