From a282a5dfb3c9d486bdc46ec8675ddde0014d3574 Mon Sep 17 00:00:00 2001 From: Elliot Date: Mon, 9 Jun 2025 11:35:15 -0500 Subject: [PATCH 1/4] add x-request-id default header --- src/utils/request.ts | 12 +++++- test/utils/request.test.ts | 84 +++++++++++++++++++++++++++++++++++++- 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/src/utils/request.ts b/src/utils/request.ts index 8ec180e..6a58123 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -5,6 +5,8 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ +import { randomUUID } from 'node:crypto'; + /** Error thrown by the SDK when receiving non-2xx responses on HTTP requests. */ export class HTTPResponseError extends Error { response: any; @@ -14,6 +16,13 @@ export class HTTPResponseError extends Error { } } +/** + * UUID generator utility wrapping node:crypto's randomUUID for stubbing in tests + */ +export const uuidGenerator = { + generate: () => randomUUID() +}; + /** * Handles HTTP requests. */ @@ -23,7 +32,7 @@ export class HttpRequestUtil { * * @param url - The URL to make the request to * @param opts - Fetch request options (method, headers, body, etc.). Headers will be merged - * with default headers `{ "User-Agent: heroku-applink-node-sdk/1.0" }` + * with default User-Agent and X-Request-ID headers. * @param json - Whether to parse the response as JSON (default: true). If false, * returns the raw Response object @@ -36,6 +45,7 @@ export class HttpRequestUtil { ...opts, headers: { "User-Agent": "heroku-applink-node-sdk/1.0", + "X-Request-Id": uuidGenerator.generate(), ...(opts?.headers ?? {}), }, }; diff --git a/test/utils/request.test.ts b/test/utils/request.test.ts index 11e3be9..be6142e 100644 --- a/test/utils/request.test.ts +++ b/test/utils/request.test.ts @@ -7,15 +7,17 @@ import { expect } from "chai"; import sinon from "sinon"; -import { HttpRequestUtil, HTTPResponseError } from "../../src/utils/request"; +import { HttpRequestUtil, HTTPResponseError, uuidGenerator } from "../../src/utils/request"; describe("HttpRequestUtil", () => { let httpRequestUtil: HttpRequestUtil; let fetchStub: sinon.SinonStub; + let uuidGeneratorStub: sinon.SinonStub; beforeEach(() => { httpRequestUtil = new HttpRequestUtil(); fetchStub = sinon.stub(global, "fetch"); + uuidGeneratorStub = sinon.stub(uuidGenerator, "generate"); }); afterEach(() => { @@ -91,7 +93,54 @@ describe("HttpRequestUtil", () => { ); }); + it("should include default request-id header", async () => { + const mockUUID = "test-uuid-1234-5678-9abc"; + uuidGeneratorStub.returns(mockUUID); + + const mockResponse = { + ok: true, + status: 200, + statusText: "OK", + json: sinon.stub().resolves({}), + }; + fetchStub.resolves(mockResponse); + + await httpRequestUtil.request("https://api.example.com/test", {}); + + const [, options] = fetchStub.getCall(0).args; + expect(options.headers["X-Request-Id"]).to.equal(mockUUID); + expect(uuidGeneratorStub.calledOnce).to.be.true; + }); + + it("should generate unique request-id for each request", async () => { + const mockUUID1 = "test-uuid-1111-1111-1111"; + const mockUUID2 = "test-uuid-2222-2222-2222"; + uuidGeneratorStub.onFirstCall().returns(mockUUID1); + uuidGeneratorStub.onSecondCall().returns(mockUUID2); + + const mockResponse = { + ok: true, + status: 200, + statusText: "OK", + json: sinon.stub().resolves({}), + }; + fetchStub.resolves(mockResponse); + + await httpRequestUtil.request("https://api.example.com/test1", {}); + await httpRequestUtil.request("https://api.example.com/test2", {}); + + const [, options1] = fetchStub.getCall(0).args; + const [, options2] = fetchStub.getCall(1).args; + + expect(options1.headers["X-Request-Id"]).to.equal(mockUUID1); + expect(options2.headers["X-Request-Id"]).to.equal(mockUUID2); + expect(uuidGeneratorStub.calledTwice).to.be.true; + }); + it("should merge custom headers with default headers", async () => { + const mockUUID = "test-uuid-merge-test"; + uuidGeneratorStub.returns(mockUUID); + const mockResponse = { ok: true, status: 200, @@ -115,11 +164,15 @@ describe("HttpRequestUtil", () => { expect(options.headers["User-Agent"]).to.equal( "heroku-applink-node-sdk/1.0" ); + expect(options.headers["X-Request-Id"]).to.equal(mockUUID); expect(options.headers["Content-Type"]).to.equal("application/json"); expect(options.headers["Authorization"]).to.equal("Bearer token123"); }); it("should allow custom headers to override User-Agent", async () => { + const mockUUID = "test-uuid-override-test"; + uuidGeneratorStub.returns(mockUUID); + const mockResponse = { ok: true, status: 200, @@ -138,6 +191,35 @@ describe("HttpRequestUtil", () => { const [, options] = fetchStub.getCall(0).args; expect(options.headers["User-Agent"]).to.equal("custom-agent/1.0"); + expect(options.headers["X-Request-Id"]).to.equal(mockUUID); + }); + + it("should allow custom headers to override request-id", async () => { + // UUID generator should still be called since it's called before merging custom headers + uuidGeneratorStub.returns("generated-uuid-that-gets-overridden"); + + const mockResponse = { + ok: true, + status: 200, + statusText: "OK", + json: sinon.stub().resolves({}), + }; + fetchStub.resolves(mockResponse); + + const customRequestId = "custom-request-id-123"; + const customOpts = { + headers: { + "X-Request-Id": customRequestId, + }, + }; + + await httpRequestUtil.request("https://api.example.com/test", customOpts); + + const [, options] = fetchStub.getCall(0).args; + expect(options.headers["X-Request-Id"]).to.equal(customRequestId); + expect(options.headers["User-Agent"]).to.equal("heroku-applink-node-sdk/1.0"); + // UUID generator should still be called since it's called before merging custom headers + expect(uuidGeneratorStub.calledOnce).to.be.true; }); it("should throw HTTPResponseError for 4xx status codes", async () => { From 1543a97271711695f0e4da3003ce1b126c40273c Mon Sep 17 00:00:00 2001 From: Elliot Date: Mon, 9 Jun 2025 12:14:20 -0500 Subject: [PATCH 2/4] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0215cde..9816361 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,3 +24,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - Update `HttpRequestUtil.request` documentation - Update license year +- Add `X-Request-Id` header From cd14e970b66e3e60de9a0c5de54f63b7787be275 Mon Sep 17 00:00:00 2001 From: Elliot Date: Mon, 9 Jun 2025 12:17:19 -0500 Subject: [PATCH 3/4] yarn format:write --- src/utils/request.ts | 4 ++-- test/utils/request.test.ts | 22 ++++++++++++++-------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/utils/request.ts b/src/utils/request.ts index 6a58123..261a9b4 100644 --- a/src/utils/request.ts +++ b/src/utils/request.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 { randomUUID } from 'node:crypto'; +import { randomUUID } from "node:crypto"; /** Error thrown by the SDK when receiving non-2xx responses on HTTP requests. */ export class HTTPResponseError extends Error { @@ -20,7 +20,7 @@ export class HTTPResponseError extends Error { * UUID generator utility wrapping node:crypto's randomUUID for stubbing in tests */ export const uuidGenerator = { - generate: () => randomUUID() + generate: () => randomUUID(), }; /** diff --git a/test/utils/request.test.ts b/test/utils/request.test.ts index be6142e..9d94f02 100644 --- a/test/utils/request.test.ts +++ b/test/utils/request.test.ts @@ -7,7 +7,11 @@ import { expect } from "chai"; import sinon from "sinon"; -import { HttpRequestUtil, HTTPResponseError, uuidGenerator } from "../../src/utils/request"; +import { + HttpRequestUtil, + HTTPResponseError, + uuidGenerator, +} from "../../src/utils/request"; describe("HttpRequestUtil", () => { let httpRequestUtil: HttpRequestUtil; @@ -96,7 +100,7 @@ describe("HttpRequestUtil", () => { it("should include default request-id header", async () => { const mockUUID = "test-uuid-1234-5678-9abc"; uuidGeneratorStub.returns(mockUUID); - + const mockResponse = { ok: true, status: 200, @@ -117,7 +121,7 @@ describe("HttpRequestUtil", () => { const mockUUID2 = "test-uuid-2222-2222-2222"; uuidGeneratorStub.onFirstCall().returns(mockUUID1); uuidGeneratorStub.onSecondCall().returns(mockUUID2); - + const mockResponse = { ok: true, status: 200, @@ -131,7 +135,7 @@ describe("HttpRequestUtil", () => { const [, options1] = fetchStub.getCall(0).args; const [, options2] = fetchStub.getCall(1).args; - + expect(options1.headers["X-Request-Id"]).to.equal(mockUUID1); expect(options2.headers["X-Request-Id"]).to.equal(mockUUID2); expect(uuidGeneratorStub.calledTwice).to.be.true; @@ -140,7 +144,7 @@ describe("HttpRequestUtil", () => { it("should merge custom headers with default headers", async () => { const mockUUID = "test-uuid-merge-test"; uuidGeneratorStub.returns(mockUUID); - + const mockResponse = { ok: true, status: 200, @@ -172,7 +176,7 @@ describe("HttpRequestUtil", () => { it("should allow custom headers to override User-Agent", async () => { const mockUUID = "test-uuid-override-test"; uuidGeneratorStub.returns(mockUUID); - + const mockResponse = { ok: true, status: 200, @@ -197,7 +201,7 @@ describe("HttpRequestUtil", () => { it("should allow custom headers to override request-id", async () => { // UUID generator should still be called since it's called before merging custom headers uuidGeneratorStub.returns("generated-uuid-that-gets-overridden"); - + const mockResponse = { ok: true, status: 200, @@ -217,7 +221,9 @@ describe("HttpRequestUtil", () => { const [, options] = fetchStub.getCall(0).args; expect(options.headers["X-Request-Id"]).to.equal(customRequestId); - expect(options.headers["User-Agent"]).to.equal("heroku-applink-node-sdk/1.0"); + expect(options.headers["User-Agent"]).to.equal( + "heroku-applink-node-sdk/1.0" + ); // UUID generator should still be called since it's called before merging custom headers expect(uuidGeneratorStub.calledOnce).to.be.true; }); From a20d8d25e840279c4175005853792d18f2053127 Mon Sep 17 00:00:00 2001 From: Elliot Date: Mon, 9 Jun 2025 15:10:19 -0500 Subject: [PATCH 4/4] Trigger Build