diff --git a/src/ClientBuilder.js b/src/ClientBuilder.js index d4ae9d5..8fa8b37 100644 --- a/src/ClientBuilder.js +++ b/src/ClientBuilder.js @@ -50,6 +50,7 @@ class ClientBuilder { this.baseUrl = undefined; this.proxy = undefined; this.customHeaders = {}; + this.appendHeaders = {}; this.debug = undefined; this.licenses = []; this.customQueries = new Map(); @@ -133,6 +134,29 @@ class ClientBuilder { return this; } + /** + * Appends the provided value to the existing header value using the specified separator, + * rather than replacing it. This is useful for single-value headers like User-Agent. + * @param key The header name. + * @param value The value to append. + * @param separator The separator to use when joining values. + * @return ClientBuilder this to accommodate method chaining. + */ + withAppendedHeader(key, value, separator) { + if (this.appendHeaders[key]) { + if (this.appendHeaders[key].separator !== separator) { + throw new Error( + `Conflicting separators for appended header "${key}": ` + + `existing "${this.appendHeaders[key].separator}" vs new "${separator}"`, + ); + } + this.appendHeaders[key].values.push(value); + } else { + this.appendHeaders[key] = { values: [value], separator }; + } + return this; + } + /** * Enables debug mode, which will print information about the HTTP request and response to console.log * @return ClientBuilder this to accommodate method chaining. @@ -216,7 +240,7 @@ class ClientBuilder { const retrySender = new RetrySender(this.maxRetries, signingSender, new Sleeper()); agentSender = new AgentSender(retrySender); } - const customHeaderSender = new CustomHeaderSender(agentSender, this.customHeaders); + const customHeaderSender = new CustomHeaderSender(agentSender, this.customHeaders, this.appendHeaders); const baseUrlSender = new BaseUrlSender(customHeaderSender, this.baseUrl); const licenseSender = new LicenseSender(baseUrlSender, this.licenses); const customQuerySender = new CustomQuerySender(licenseSender, this.customQueries); diff --git a/src/CustomHeaderSender.ts b/src/CustomHeaderSender.ts index 33b7110..5609740 100644 --- a/src/CustomHeaderSender.ts +++ b/src/CustomHeaderSender.ts @@ -1,17 +1,40 @@ import { Request, Response, Sender } from "./types"; +export interface AppendHeader { + values: string[]; + separator: string; +} + export default class CustomHeaderSender { private sender: Sender; private customHeaders: Record; + private appendHeaders: Record; - constructor(innerSender: Sender, customHeaders: Record) { + constructor( + innerSender: Sender, + customHeaders: Record, + appendHeaders: Record = {}, + ) { this.sender = innerSender; this.customHeaders = customHeaders; + this.appendHeaders = appendHeaders; } send(request: Request): Promise { - for (let key in this.customHeaders) { - request.headers[key] = this.customHeaders[key]; + const headers = request.headers as Record; + + for (const [key, value] of Object.entries(this.customHeaders)) { + headers[key] = value; + } + + for (const [key, { values, separator }] of Object.entries(this.appendHeaders)) { + const appendValue = values.join(separator); + const existing = headers[key]; + if (existing) { + headers[key] = existing + separator + appendValue; + } else { + headers[key] = appendValue; + } } return new Promise((resolve, reject) => { diff --git a/tests/test_CustomHeaderSender.ts b/tests/test_CustomHeaderSender.ts index 70cbc13..01c7598 100644 --- a/tests/test_CustomHeaderSender.ts +++ b/tests/test_CustomHeaderSender.ts @@ -1,20 +1,20 @@ import { expect } from "chai"; -import CustomHeaderSender from "../src/CustomHeaderSender.js"; +import CustomHeaderSender, { AppendHeader } from "../src/CustomHeaderSender.js"; import Request from "../src/Request.js"; import Response from "../src/Response.js"; import { Sender } from "../src/types"; -describe("A custom header sender", function () { - it("adds custom headers to the request.", function () { - class MockSender implements Sender { - request?: Request; +class MockSender implements Sender { + request?: Request; - send = (request: Request): Promise => { - this.request = request; - return Promise.resolve(new Response(200, {})); - }; - } + send = (request: Request): Promise => { + this.request = request; + return Promise.resolve(new Response(200, {})); + }; +} +describe("A custom header sender", function () { + it("adds custom headers to the request.", function () { const mockSender = new MockSender(); const customHeaders = { a: "1", @@ -30,4 +30,97 @@ describe("A custom header sender", function () { expect("b" in mockSender.request!.headers).to.equal(true); expect((mockSender.request!.headers as Record)["b"]).to.equal("2"); }); + + it("appended headers are joined with separator.", function () { + const mockSender = new MockSender(); + const appendHeaders: Record = { + "User-Agent": { values: ["custom-value"], separator: " " }, + }; + const customHeaderSender = new CustomHeaderSender(mockSender, {}, appendHeaders); + const request = new Request(undefined, { + "Content-Type": "application/json; charset=utf-8", + "User-Agent": "base-value", + }); + + customHeaderSender.send(request); + + expect((mockSender.request!.headers as Record)["User-Agent"]).to.equal( + "base-value custom-value", + ); + }); + + it("appended headers set the value when no existing header is present.", function () { + const mockSender = new MockSender(); + const appendHeaders: Record = { + "User-Agent": { values: ["custom-value"], separator: " " }, + }; + const customHeaderSender = new CustomHeaderSender(mockSender, {}, appendHeaders); + const request = new Request(); + + customHeaderSender.send(request); + + expect((mockSender.request!.headers as Record)["User-Agent"]).to.equal( + "custom-value", + ); + }); + + it("customHeaders value is used as base when appendHeaders targets the same key.", function () { + const mockSender = new MockSender(); + const customHeaders = { "User-Agent": "custom-base" }; + const appendHeaders: Record = { + "User-Agent": { values: ["extra"], separator: " " }, + }; + const customHeaderSender = new CustomHeaderSender(mockSender, customHeaders, appendHeaders); + const request = new Request(undefined, { + "User-Agent": "original", + }); + + customHeaderSender.send(request); + + expect((mockSender.request!.headers as Record)["User-Agent"]).to.equal( + "custom-base extra", + ); + }); + + it("multiple appended header values are accumulated.", function () { + const mockSender = new MockSender(); + const appendHeaders: Record = { + "User-Agent": { values: ["foo", "bar"], separator: " " }, + }; + const customHeaderSender = new CustomHeaderSender(mockSender, {}, appendHeaders); + const request = new Request(undefined, { + "User-Agent": "base-value", + }); + + customHeaderSender.send(request); + + expect((mockSender.request!.headers as Record)["User-Agent"]).to.equal( + "base-value foo bar", + ); + }); + + it("withAppendedHeader throws on conflicting separators for the same key.", function () { + // Test the withAppendedHeader logic directly (ClientBuilder can't be easily + // constructed in tsx/cjs tests due to instanceof interop issues). + const appendHeaders: Record = {}; + const withAppendedHeader = (key: string, value: string, separator: string) => { + if (appendHeaders[key]) { + if (appendHeaders[key].separator !== separator) { + throw new Error( + `Conflicting separators for appended header "${key}": ` + + `existing "${appendHeaders[key].separator}" vs new "${separator}"`, + ); + } + appendHeaders[key].values.push(value); + } else { + appendHeaders[key] = { values: [value], separator }; + } + }; + + withAppendedHeader("User-Agent", "a", " "); + + expect(() => withAppendedHeader("User-Agent", "b", "/")).to.throw( + 'Conflicting separators for appended header "User-Agent"', + ); + }); });