Skip to content
Merged
2 changes: 2 additions & 0 deletions sdk/core/core-rest-pipeline/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Features Added

- Changed behavior when sending HTTP headers to preserve the original casing of header names. Iterating over `HttpHeaders` now keeps the original name casing. There is also a new `preserveCase` option for `HttpHeaders.toJSON()`. See [PR #18517](https://github.com/Azure/azure-sdk-for-js/pull/18517)

### Breaking Changes

### Bugs Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ export interface HttpHeaders extends Iterable<[string, string]> {
get(name: string): string | undefined;
has(name: string): boolean;
set(name: string, value: string | number | boolean): void;
toJSON(): RawHttpHeaders;
toJSON(options?: {
preserveCase?: boolean;
}): RawHttpHeaders;
}

// @public
Expand Down
36 changes: 27 additions & 9 deletions sdk/core/core-rest-pipeline/src/httpHeaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,26 @@

import { HttpHeaders, RawHttpHeaders, RawHttpHeadersInput } from "./interfaces";

interface HeaderEntry {
name: string;
value: string;
}

function normalizeName(name: string): string {
return name.toLowerCase();
}

function* headerIterator(map: Map<string, HeaderEntry>): IterableIterator<[string, string]> {
for (const entry of map.values()) {
yield [entry.name, entry.value];
}
}

class HttpHeadersImpl implements HttpHeaders {
private readonly _headersMap: Map<string, string>;
private readonly _headersMap: Map<string, HeaderEntry>;

constructor(rawHeaders?: RawHttpHeaders | RawHttpHeadersInput) {
this._headersMap = new Map<string, string>();
this._headersMap = new Map<string, HeaderEntry>();
if (rawHeaders) {
for (const headerName of Object.keys(rawHeaders)) {
this.set(headerName, rawHeaders[headerName]);
Expand All @@ -26,7 +37,7 @@ class HttpHeadersImpl implements HttpHeaders {
* @param value - The value of the header to set.
*/
public set(name: string, value: string | number | boolean): void {
this._headersMap.set(normalizeName(name), String(value));
this._headersMap.set(normalizeName(name), { name, value: String(value) });
}

/**
Expand All @@ -35,7 +46,7 @@ class HttpHeadersImpl implements HttpHeaders {
* @param name - The name of the header. This value is case-insensitive.
*/
public get(name: string): string | undefined {
return this._headersMap.get(normalizeName(name));
return this._headersMap.get(normalizeName(name))?.value;
}

/**
Expand All @@ -57,26 +68,33 @@ class HttpHeadersImpl implements HttpHeaders {
/**
* Get the JSON object representation of this HTTP header collection.
*/
public toJSON(): RawHttpHeaders {
public toJSON(options: { preserveCase?: boolean } = {}): RawHttpHeaders {
const result: RawHttpHeaders = {};
for (const [key, value] of this._headersMap) {
result[key] = value;
if (options.preserveCase) {
for (const entry of this._headersMap.values()) {
result[entry.name] = entry.value;
}
} else {
for (const [normalizedName, entry] of this._headersMap) {
result[normalizedName] = entry.value;
}
}

return result;
}

/**
* Get the string representation of this HTTP header collection.
*/
public toString(): string {
return JSON.stringify(this.toJSON());
return JSON.stringify(this.toJSON({ preserveCase: true }));
}

/**
* Iterate over tuples of header [name, value] pairs.
*/
[Symbol.iterator](): Iterator<[string, string]> {
return this._headersMap.entries();
return headerIterator(this._headersMap);
}
}

Expand Down
2 changes: 1 addition & 1 deletion sdk/core/core-rest-pipeline/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export interface HttpHeaders extends Iterable<[string, string]> {
* Accesses a raw JS object that acts as a simple map
* of header names to values.
*/
toJSON(): RawHttpHeaders;
toJSON(options?: { preserveCase?: boolean }): RawHttpHeaders;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion sdk/core/core-rest-pipeline/src/nodeHttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ class NodeHttpClient implements HttpClient {
path: `${url.pathname}${url.search}`,
port: url.port,
method: request.method,
headers: request.headers.toJSON()
headers: request.headers.toJSON({ preserveCase: true })
};

return new Promise<http.IncomingMessage>((resolve, reject) => {
Expand Down
47 changes: 47 additions & 0 deletions sdk/core/core-rest-pipeline/test/httpHeaders.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { assert } from "chai";
import { createHttpHeaders } from "../src/httpHeaders";

describe("HttpHeaders", () => {
it("toJSON() should use normalized header names", () => {
const rawHeaders = {
lowercase: "lower case value",
camelCase: "camel case value",
ALLUPPERCASE: "all upper case value"
};
const normalizedHeaders = {
lowercase: "lower case value",
camelcase: "camel case value",
alluppercase: "all upper case value"
};
const headers = createHttpHeaders(rawHeaders);

assert.deepStrictEqual(headers.toJSON(), normalizedHeaders);
});

it("toJSON({preserveCase: true}) should keep the original header names", () => {
const rawHeaders = {
lowercase: "lower case value",
camelCase: "camel case value",
ALLUPPERCASE: "all upper case value"
};
const headers = createHttpHeaders(rawHeaders);

assert.deepStrictEqual(headers.toJSON({ preserveCase: true }), rawHeaders);
});

it("iteration should use original header names", () => {
const rawHeaders = {
lowercase: "lower case value",
camelCase: "camel case value",
ALLUPPERCASE: "all upper case value"
};
const headers = createHttpHeaders(rawHeaders);

for (const [name, value] of headers) {
assert.include(rawHeaders, { [name]: value });
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ function prepareRequestOptions(
method: "GET",
headers: createHttpHeaders({
Accept: "application/json",
Secret: process.env.IDENTITY_HEADER
secret: process.env.IDENTITY_HEADER
})
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ describe("ManagedIdentityCredential", function() {
"URL does not start with expected host and path"
);

assert.equal(authRequest.headers.authorization, `Basic ${key}`);
assert.equal(authRequest.headers.Authorization, `Basic ${key}`);
if (authDetails.result!.token) {
// We use Date.now underneath.
assert.ok(authDetails.result!.expiresOnTimestamp);
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading