Skip to content

Commit 940990a

Browse files
committed
add mergeHeaders until we get to the point or removing legacy options
1 parent b2fccda commit 940990a

5 files changed

Lines changed: 206 additions & 12 deletions

File tree

packages/arcgis-rest-request/src/request.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { IRetryAuthError } from "./utils/retryAuthError.js";
1919
import { IAuthenticationManager } from "./index.js";
2020
import { isSameOrigin } from "./utils/isSameOrigin.js";
2121
import { normalizeDeprecatedRequestOptions } from "./utils/normalize-deprecated-request-options.js";
22+
import { mergeHeaders } from "./utils/merge-headers.js";
2223

2324
export const NODEJS_DEFAULT_REFERER_HEADER = `@esri/arcgis-rest-js`;
2425
const ENTERPRISE_MAX_URL_LENGTH = 2000;
@@ -237,10 +238,10 @@ function normalizeRequestOptions(
237238
fetchOptions: {
238239
...defaults.fetchOptions,
239240
...normalizedRequestOptions.fetchOptions,
240-
headers: {
241-
...(defaults.fetchOptions?.headers as any),
242-
...(normalizedRequestOptions.fetchOptions?.headers as any)
243-
}
241+
headers: mergeHeaders(
242+
defaults.fetchOptions?.headers,
243+
normalizedRequestOptions.fetchOptions?.headers
244+
)
244245
}
245246
}
246247
};
@@ -468,10 +469,7 @@ https://developers.arcgis.com/rest/users-groups-and-items/update-resources.htm
468469
}
469470

470471
// Mixin headers from request options
471-
fetchOptions.headers = {
472-
...requestHeaders,
473-
...(fetchOptions.headers as any)
474-
};
472+
fetchOptions.headers = mergeHeaders(requestHeaders, fetchOptions.headers);
475473

476474
// This should have the same conditional for Node JS as ArcGISIdentityManager.refreshWithUsernameAndPassword()
477475
// to ensure that generated tokens have the same referer when used in Node with a username and password.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/* Copyright (c) 2026 Environmental Systems Research Institute, Inc.
2+
* Apache-2.0 */
3+
4+
type HeaderMap = {
5+
[key: string]: string;
6+
};
7+
8+
function appendHeaders(target: HeaderMap, headers?: HeadersInit): void {
9+
if (!headers) {
10+
return;
11+
}
12+
13+
if (Array.isArray(headers)) {
14+
headers.forEach(([key, value]) => {
15+
target[key] = String(value);
16+
});
17+
return;
18+
}
19+
20+
if (typeof Headers !== "undefined" && headers instanceof Headers) {
21+
headers.forEach((value, key) => {
22+
target[key] = value;
23+
});
24+
return;
25+
}
26+
27+
Object.entries(headers).forEach(([key, value]) => {
28+
if (typeof value !== "undefined") {
29+
target[key] = String(value);
30+
}
31+
});
32+
}
33+
34+
export function mergeHeaders(...headerSets: Array<HeadersInit | undefined>) {
35+
const mergedHeaders: HeaderMap = {};
36+
37+
headerSets.forEach((headers) => {
38+
appendHeaders(mergedHeaders, headers);
39+
});
40+
41+
return mergedHeaders;
42+
}

packages/arcgis-rest-request/src/utils/normalize-deprecated-request-options.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Apache-2.0 */
33

44
import { IRequestOptions } from "./IRequestOptions.js";
5+
import { mergeHeaders } from "./merge-headers.js";
56

67
/**
78
* Converts deprecated top-level request options into their v2 IRequestOptions
@@ -32,10 +33,7 @@ export function normalizeDeprecatedRequestOptions(
3233
...(credentials !== undefined ? { credentials } : {}),
3334
...(signal !== undefined ? { signal } : {}),
3435
...requestOptions.fetchOptions,
35-
headers: {
36-
...(headers as any),
37-
...(requestOptions.fetchOptions?.headers as any)
38-
}
36+
headers: mergeHeaders(headers as any, requestOptions.fetchOptions?.headers)
3937
};
4038

4139
const normalizedOptions: IRequestOptions = {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/* Copyright (c) 2026 Environmental Systems Research Institute, Inc.
2+
* Apache-2.0 */
3+
4+
import { describe, expect, test } from "vitest";
5+
import { mergeHeaders } from "../../src/utils/merge-headers.js";
6+
7+
describe("mergeHeaders", () => {
8+
test("should merge plain object headers", () => {
9+
const merged = mergeHeaders(
10+
{
11+
"X-Test": "from-defaults",
12+
"X-Defaults-Only": "true"
13+
},
14+
{
15+
"X-Test": "from-request",
16+
"X-Request-Only": "true"
17+
}
18+
);
19+
20+
expect(merged).toEqual({
21+
"X-Test": "from-request",
22+
"X-Defaults-Only": "true",
23+
"X-Request-Only": "true"
24+
});
25+
});
26+
27+
test("should merge tuple headers", () => {
28+
const merged = mergeHeaders([
29+
["X-Token", "abc123"],
30+
["X-Custom", "custom-value"]
31+
]);
32+
33+
expect(merged).toEqual({
34+
"X-Token": "abc123",
35+
"X-Custom": "custom-value"
36+
});
37+
});
38+
39+
test("should merge Headers instance", () => {
40+
if (typeof Headers === "undefined") {
41+
return;
42+
}
43+
44+
const headerObject = mergeHeaders(
45+
{ "X-Initial": "true" },
46+
new Headers([["X-From-Headers", "true"]])
47+
);
48+
49+
expect(headerObject["X-Initial"]).toBe("true");
50+
expect(headerObject["x-from-headers"]).toBe("true");
51+
});
52+
53+
test("should omit undefined object header values", () => {
54+
const headerObject = mergeHeaders({
55+
"X-Defined": "true",
56+
"X-Undefined": undefined as any
57+
});
58+
59+
expect(headerObject).toEqual({
60+
"X-Defined": "true"
61+
});
62+
expect(headerObject["X-Undefined"]).toBeUndefined();
63+
});
64+
65+
test("should ignore undefined header sets", () => {
66+
const headerObject = mergeHeaders(undefined, {
67+
"X-Defined": "true"
68+
});
69+
70+
expect(headerObject).toEqual({
71+
"X-Defined": "true"
72+
});
73+
});
74+
});

packages/arcgis-rest-request/test/utils/normalize-deprecated-request-options.test.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,88 @@ describe("normalizeDeprecatedRequestOptions", () => {
9494
});
9595
});
9696

97+
test("should normalize tuple-based fetch headers", () => {
98+
const normalized = normalizeDeprecatedRequestOptions({
99+
headers: {
100+
"X-Deprecated": "legacy"
101+
},
102+
fetchOptions: {
103+
headers: [["X-Fetch-Tuple", "tuple-value"]]
104+
}
105+
});
106+
107+
expect(normalized.fetchOptions?.headers).toEqual({
108+
"X-Deprecated": "legacy",
109+
"X-Fetch-Tuple": "tuple-value"
110+
});
111+
});
112+
113+
test("should prefer tuple-based fetch headers over deprecated top-level headers", () => {
114+
const normalized = normalizeDeprecatedRequestOptions({
115+
headers: {
116+
"X-Test": "legacy-value",
117+
"X-Deprecated-Only": "legacy"
118+
},
119+
fetchOptions: {
120+
headers: [
121+
["X-Test", "tuple-value"],
122+
["X-Fetch-Only", "tuple-only"]
123+
]
124+
}
125+
});
126+
127+
expect(normalized.fetchOptions?.headers).toEqual({
128+
"X-Test": "tuple-value",
129+
"X-Deprecated-Only": "legacy",
130+
"X-Fetch-Only": "tuple-only"
131+
});
132+
});
133+
134+
test("should normalize Headers instance in fetchOptions.headers", () => {
135+
if (typeof Headers === "undefined") {
136+
return;
137+
}
138+
139+
const normalized = normalizeDeprecatedRequestOptions({
140+
headers: {
141+
"X-Deprecated": "legacy"
142+
},
143+
fetchOptions: {
144+
headers: new Headers([["X-Fetch-Headers", "headers-value"]])
145+
}
146+
});
147+
148+
expect(normalized.fetchOptions?.headers).toEqual({
149+
"X-Deprecated": "legacy",
150+
"x-fetch-headers": "headers-value"
151+
});
152+
});
153+
154+
test("should prefer Headers instance values over deprecated top-level headers", () => {
155+
if (typeof Headers === "undefined") {
156+
return;
157+
}
158+
159+
const normalized = normalizeDeprecatedRequestOptions({
160+
headers: {
161+
"x-test": "legacy-value",
162+
"X-Deprecated-Only": "legacy"
163+
},
164+
fetchOptions: {
165+
headers: new Headers([
166+
["X-Test", "headers-value"],
167+
["X-Fetch-Only", "headers-only"]
168+
])
169+
}
170+
});
171+
172+
expect(normalized.fetchOptions?.headers).toEqual({
173+
"x-test": "headers-value",
174+
"X-Deprecated-Only": "legacy",
175+
"x-fetch-only": "headers-only"
176+
});
177+
});
178+
97179
test("should drop legacy options without v2 replacements", () => {
98180
const normalized = normalizeDeprecatedRequestOptions({
99181
maxUrlLength: 3000,

0 commit comments

Comments
 (0)