Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion src/ClientBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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 <b>this</b> 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 <b>this</b> to accommodate method chaining.
Expand Down Expand Up @@ -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);
Expand Down
29 changes: 26 additions & 3 deletions src/CustomHeaderSender.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>;
private appendHeaders: Record<string, AppendHeader>;

constructor(innerSender: Sender, customHeaders: Record<string, string>) {
constructor(
innerSender: Sender,
customHeaders: Record<string, string>,
appendHeaders: Record<string, AppendHeader> = {},
) {
this.sender = innerSender;
this.customHeaders = customHeaders;
this.appendHeaders = appendHeaders;
}

send(request: Request): Promise<Response> {
for (let key in this.customHeaders) {
request.headers[key] = this.customHeaders[key];
const headers = request.headers as Record<string, string>;

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) => {
Expand Down
113 changes: 103 additions & 10 deletions tests/test_CustomHeaderSender.ts
Original file line number Diff line number Diff line change
@@ -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<Response> => {
this.request = request;
return Promise.resolve(new Response(200, {}));
};
}
send = (request: Request): Promise<Response> => {
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",
Expand All @@ -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<string, string>)["b"]).to.equal("2");
});

it("appended headers are joined with separator.", function () {
const mockSender = new MockSender();
const appendHeaders: Record<string, AppendHeader> = {
"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<string, string>)["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<string, AppendHeader> = {
"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<string, string>)["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<string, AppendHeader> = {
"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<string, string>)["User-Agent"]).to.equal(
"custom-base extra",
);
});

it("multiple appended header values are accumulated.", function () {
const mockSender = new MockSender();
const appendHeaders: Record<string, AppendHeader> = {
"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<string, string>)["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<string, AppendHeader> = {};
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"',
);
});
});