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"',
+ );
+ });
});