From 7b7c72ceee264951556a7355053405733c305c40 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Thu, 26 Mar 2026 11:57:53 -0600 Subject: [PATCH 1/3] Added wrapped http sender option for custom HTTP transport with middleware chain. --- src/ClientBuilder.ts | 9 ++++++++- tests/test_ClientBuilder.ts | 22 ++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/ClientBuilder.ts b/src/ClientBuilder.ts index c98d571..07d1d35 100644 --- a/src/ClientBuilder.ts +++ b/src/ClientBuilder.ts @@ -41,6 +41,7 @@ type Signer = StaticCredentials | SharedCredentials | BasicAuthCredentials; export default class ClientBuilder { private signer: Signer; private httpSender: Sender | undefined; + private wrappedHttpSender: Sender | undefined; private maxRetries: number; private maxTimeout: number; private baseUrl: string | undefined; @@ -63,6 +64,7 @@ export default class ClientBuilder { this.signer = signer; this.httpSender = undefined; + this.wrappedHttpSender = undefined; this.maxRetries = 5; this.maxTimeout = 10000; this.baseUrl = undefined; @@ -89,6 +91,11 @@ export default class ClientBuilder { return this; } + withWrappedSender(sender: Sender): ClientBuilder { + this.wrappedHttpSender = sender; + return this; + } + withBaseUrl(url: string): ClientBuilder { this.baseUrl = url; return this; @@ -181,7 +188,7 @@ export default class ClientBuilder { buildSender(): Sender { if (this.httpSender) return this.httpSender; - const httpSender = new HttpSender(this.maxTimeout, this.proxy, this.debug); + const httpSender = this.wrappedHttpSender ?? new HttpSender(this.maxTimeout, this.proxy, this.debug); const statusCodeSender = new StatusCodeSender(httpSender); const signingSender = new SigningSender(statusCodeSender, this.signer); let agentSender = new AgentSender(signingSender); diff --git a/tests/test_ClientBuilder.ts b/tests/test_ClientBuilder.ts index 189c6cb..40ecd3c 100644 --- a/tests/test_ClientBuilder.ts +++ b/tests/test_ClientBuilder.ts @@ -1,6 +1,8 @@ import { expect } from "chai"; import StaticCredentials from "../src/StaticCredentials.js"; import ClientBuilder from "../src/ClientBuilder.js"; +import Lookup from "../src/us_street/Lookup.js"; +import { Request as IRequest, Response as IResponse, Sender } from "../src/types.js"; describe("ClientBuilder", function () { const credentials = new StaticCredentials("test-id", "test-token"); @@ -21,4 +23,24 @@ describe("ClientBuilder", function () { "component-analysis,iana-timezone", ); }); + + it("wraps a custom http sender with the full middleware chain (baseUrl and auth are set).", async function () { + let capturedRequest: IRequest | undefined; + const capturingSender: Sender = { + send(request: IRequest): Promise { + capturedRequest = request; + return Promise.resolve({ statusCode: 200, payload: [], error: null, headers: {} }); + }, + }; + + const client = new ClientBuilder(credentials).withWrappedSender(capturingSender).buildUsStreetApiClient(); + + const lookup = new Lookup(); + lookup.street = "1 Rosedale"; + await client.send(lookup); + + expect(capturedRequest!.baseUrl).to.include("us-street.api.smarty.com"); + expect(capturedRequest!.parameters["auth-id"]).to.equal("test-id"); + expect(capturedRequest!.parameters["auth-token"]).to.equal("test-token"); + }); }); From 06ee3309bb5cbdc7f93c4a36c9df9b40efcb6280 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 27 Mar 2026 10:54:56 -0600 Subject: [PATCH 2/3] Changed withSender to wrap provided sender in the middleware chain. --- src/ClientBuilder.ts | 11 +---------- tests/test_ClientBuilder.ts | 2 +- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/ClientBuilder.ts b/src/ClientBuilder.ts index 07d1d35..c259b60 100644 --- a/src/ClientBuilder.ts +++ b/src/ClientBuilder.ts @@ -41,7 +41,6 @@ type Signer = StaticCredentials | SharedCredentials | BasicAuthCredentials; export default class ClientBuilder { private signer: Signer; private httpSender: Sender | undefined; - private wrappedHttpSender: Sender | undefined; private maxRetries: number; private maxTimeout: number; private baseUrl: string | undefined; @@ -64,7 +63,6 @@ export default class ClientBuilder { this.signer = signer; this.httpSender = undefined; - this.wrappedHttpSender = undefined; this.maxRetries = 5; this.maxTimeout = 10000; this.baseUrl = undefined; @@ -91,11 +89,6 @@ export default class ClientBuilder { return this; } - withWrappedSender(sender: Sender): ClientBuilder { - this.wrappedHttpSender = sender; - return this; - } - withBaseUrl(url: string): ClientBuilder { this.baseUrl = url; return this; @@ -186,9 +179,7 @@ export default class ClientBuilder { } buildSender(): Sender { - if (this.httpSender) return this.httpSender; - - const httpSender = this.wrappedHttpSender ?? new HttpSender(this.maxTimeout, this.proxy, this.debug); + const httpSender = this.httpSender ?? new HttpSender(this.maxTimeout, this.proxy, this.debug); const statusCodeSender = new StatusCodeSender(httpSender); const signingSender = new SigningSender(statusCodeSender, this.signer); let agentSender = new AgentSender(signingSender); diff --git a/tests/test_ClientBuilder.ts b/tests/test_ClientBuilder.ts index 40ecd3c..2500876 100644 --- a/tests/test_ClientBuilder.ts +++ b/tests/test_ClientBuilder.ts @@ -33,7 +33,7 @@ describe("ClientBuilder", function () { }, }; - const client = new ClientBuilder(credentials).withWrappedSender(capturingSender).buildUsStreetApiClient(); + const client = new ClientBuilder(credentials).withSender(capturingSender).buildUsStreetApiClient(); const lookup = new Lookup(); lookup.street = "1 Rosedale"; From 72dd0ccc92f1c7a2756ba4dde952614d5a4aa9d9 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 27 Mar 2026 11:09:39 -0600 Subject: [PATCH 3/3] Added conflict validation when withSender() is used with native-transport-only options. --- src/ClientBuilder.ts | 8 ++++++++ tests/test_ClientBuilder.ts | 12 ++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/ClientBuilder.ts b/src/ClientBuilder.ts index c259b60..aecfecd 100644 --- a/src/ClientBuilder.ts +++ b/src/ClientBuilder.ts @@ -179,6 +179,14 @@ export default class ClientBuilder { } buildSender(): Sender { + if (this.httpSender !== undefined) { + const conflicts: string[] = []; + if (this.maxTimeout !== 10000) conflicts.push("withMaxTimeout()"); + if (this.proxy !== undefined) conflicts.push("withProxy()"); + if (this.debug !== undefined) conflicts.push("withDebug()"); + if (conflicts.length > 0) + throw new Error(`withSender() cannot be combined with: ${conflicts.join(", ")}. These options only apply to the built-in HTTP transport.`); + } const httpSender = this.httpSender ?? new HttpSender(this.maxTimeout, this.proxy, this.debug); const statusCodeSender = new StatusCodeSender(httpSender); const signingSender = new SigningSender(statusCodeSender, this.signer); diff --git a/tests/test_ClientBuilder.ts b/tests/test_ClientBuilder.ts index 2500876..a6b7dbb 100644 --- a/tests/test_ClientBuilder.ts +++ b/tests/test_ClientBuilder.ts @@ -24,6 +24,18 @@ describe("ClientBuilder", function () { ); }); + it("throws when withSender() is combined with withMaxTimeout().", function () { + expect(() => + new ClientBuilder(credentials).withSender({ send: async () => ({ statusCode: 200, payload: [], error: null, headers: {} }) }).withMaxTimeout(5000).buildUsStreetApiClient() + ).to.throw("withSender() cannot be combined with: withMaxTimeout()"); + }); + + it("throws when withSender() is combined with withProxy().", function () { + expect(() => + new ClientBuilder(credentials).withSender({ send: async () => ({ statusCode: 200, payload: [], error: null, headers: {} }) }).withProxy({ host: "localhost", port: 8080 }).buildUsStreetApiClient() + ).to.throw("withSender() cannot be combined with: withProxy()"); + }); + it("wraps a custom http sender with the full middleware chain (baseUrl and auth are set).", async function () { let capturedRequest: IRequest | undefined; const capturingSender: Sender = {