@ts-common/azure-js-dev-tools
Version:
Developer dependencies for TypeScript related projects
365 lines (325 loc) • 10.5 kB
text/typescript
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for
* license information.
*/
import * as http from "http";
import * as https from "https";
import { last } from "./arrays";
import { URLBuilder } from "./url";
/**
* A collection of HttpHeaders that can be sent with a HTTP request.
*/
export function getHeaderKey(headerName: string) {
return headerName.toLowerCase();
}
/**
* An individual header within a HttpHeaders collection.
*/
export interface HttpHeader {
/**
* The name of the header.
*/
name: string;
/**
* The value of the header.
*/
value: string;
}
/**
* A HttpHeaders collection represented as a simple JSON object.
*/
export type RawHttpHeaders = { [headerName: string]: string };
/**
* A collection of HTTP header key/value pairs.
*/
export class HttpHeaders {
private readonly _headersMap: { [headerKey: string]: HttpHeader };
constructor(rawHeaders?: RawHttpHeaders) {
this._headersMap = {};
if (rawHeaders) {
for (const headerName in rawHeaders) {
this.set(headerName, rawHeaders[headerName]);
}
}
}
/**
* Set a header in this collection with the provided name and value. The name is
* case-insensitive.
* @param headerName The name of the header to set. This value is case-insensitive.
* @param headerValue The value of the header to set.
*/
public set(headerName: string, headerValue: string | number): void {
this._headersMap[getHeaderKey(headerName)] = { name: headerName, value: headerValue.toString() };
}
/**
* Get the header value for the provided header name, or undefined if no header exists in this
* collection with the provided name.
* @param headerName The name of the header.
*/
public get(headerName: string): string | undefined {
const header: HttpHeader = this._headersMap[getHeaderKey(headerName)];
return !header ? undefined : header.value;
}
/**
* Get whether or not this header collection contains a header entry for the provided header name.
*/
public contains(headerName: string): boolean {
return !!this._headersMap[getHeaderKey(headerName)];
}
/**
* Remove the header with the provided headerName. Return whether or not the header existed and
* was removed.
* @param headerName The name of the header to remove.
*/
public remove(headerName: string): boolean {
const result: boolean = this.contains(headerName);
delete this._headersMap[getHeaderKey(headerName)];
return result;
}
/**
* Get the headers that are contained this collection as an object.
*/
public rawHeaders(): RawHttpHeaders {
const result: RawHttpHeaders = {};
for (const headerKey in this._headersMap) {
const header: HttpHeader = this._headersMap[headerKey];
result[header.name.toLowerCase()] = header.value;
}
return result;
}
/**
* Get the headers that are contained in this collection as an array.
*/
public headersArray(): HttpHeader[] {
const headers: HttpHeader[] = [];
for (const headerKey in this._headersMap) {
headers.push(this._headersMap[headerKey]);
}
return headers;
}
/**
* Get the header names that are contained in this collection.
*/
public headerNames(): string[] {
const headerNames: string[] = [];
const headers: HttpHeader[] = this.headersArray();
for (let i = 0; i < headers.length; ++i) {
headerNames.push(headers[i].name);
}
return headerNames;
}
/**
* Get the header names that are contained in this collection.
*/
public headerValues(): string[] {
const headerValues: string[] = [];
const headers: HttpHeader[] = this.headersArray();
for (let i = 0; i < headers.length; ++i) {
headerValues.push(headers[i].value);
}
return headerValues;
}
/**
* Get the JSON object representation of this HTTP header collection.
*/
public toJson(): RawHttpHeaders {
return this.rawHeaders();
}
/**
* Get the string representation of this HTTP header collection.
*/
public toString(): string {
return JSON.stringify(this.toJson());
}
/**
* Create a deep clone/copy of this HttpHeaders collection.
*/
public clone(): HttpHeaders {
return new HttpHeaders(this.rawHeaders());
}
}
export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS";
/**
* An HTTP request to send to an HTTP server.
*/
export interface HttpRequest {
/**
* The HTTP method that this request will be sent with.
*/
method: HttpMethod;
/**
* The URL that this request will be sent to.
*/
url: string | URLBuilder;
/**
* The headers that this request with be sent with.
*/
headers?: HttpHeaders | RawHttpHeaders;
/**
* The body to send with this request.
*/
body?: any;
}
/**
* An HTTP response received from an HTTP server.
*/
export interface HttpResponse {
/**
* The request that this response is responding to.
*/
request: HttpRequest;
/**
* The response status code.
*/
statusCode: number;
/**
* The response headers.
*/
headers: HttpHeaders;
/**
* The response body.
*/
body?: string;
}
/**
* An interface that can send HttpRequests and receive promised HttpResponses.
*/
export interface HttpClient {
/**
* Send the provided HttpRequest and get back an HttpResponse.
* @param request The HttpRequest to send.
*/
sendRequest(request: HttpRequest): Promise<HttpResponse>;
}
/**
* Get an instance of the default HttpClient.
*/
export function getDefaultHttpClient(): HttpClient {
return new NodeHttpClient();
}
function sendNodeHttpClientRequest(request: HttpRequest): Promise<HttpResponse> {
const requestUrl: URLBuilder = request.url instanceof URLBuilder ? request.url : URLBuilder.parse(request.url);
let requestHeaders: http.OutgoingHttpHeaders | undefined;
if (request.headers instanceof HttpHeaders) {
requestHeaders = request.headers.rawHeaders();
} else if (request.headers) {
requestHeaders = request.headers;
}
const protocol: string = requestUrl.getScheme() || "http";
const requestOptions: http.RequestOptions = {
method: request.method,
headers: requestHeaders,
protocol: protocol + ":",
host: requestUrl.getHost(),
port: requestUrl.getPort(),
path: (requestUrl.getPath() || "") + (requestUrl.getQuery() || "")
};
return new Promise((resolve, reject) => {
try {
const clientRequest: http.ClientRequest = (protocol === "http" ? http : https).request(requestOptions, (response: http.IncomingMessage) => {
try {
response.setEncoding("utf8");
let responseBody = "";
response.on("data", (chunk: any) => {
responseBody += chunk;
});
response.on("end", () => {
const responseHeaders = new HttpHeaders();
for (const responseHeaderName of Object.keys(response.headers)) {
const responseHeaderValue: string | string[] | undefined = response.headers[responseHeaderName];
if (typeof responseHeaderValue === "string") {
responseHeaders.set(responseHeaderName, responseHeaderValue);
} else if (Array.isArray(responseHeaderValue)) {
responseHeaders.set(responseHeaderName, responseHeaderValue.join(","));
}
}
resolve({
request,
statusCode: response.statusCode!,
headers: responseHeaders,
body: responseBody
});
});
response.on("error", (error: Error) => {
reject(error);
});
} catch (error) {
reject(error);
}
});
if (request.body) {
clientRequest.write(request.body, (error: Error | null | undefined) => {
if (error) {
reject(error);
}
});
}
clientRequest.on("error", (error: Error) => {
reject(error);
});
clientRequest.end(() => {});
} catch (error) {
reject(error);
}
});
}
/**
* Options that can be provided when creating a new NodeHttpClient.
*/
export interface NodeHttpClientOptions {
/**
* Whether or not redirects will be automatically handled. Defaults to true.
*/
handleRedirects?: boolean;
}
/**
* An HTTP client that uses the built-in Node.js http module.
*/
export class NodeHttpClient implements HttpClient {
private readonly handleRedirects: boolean;
constructor(options: NodeHttpClientOptions = {}) {
this.handleRedirects = options.handleRedirects == undefined ? true : options.handleRedirects;
}
public async sendRequest(request: HttpRequest): Promise<HttpResponse> {
let response: HttpResponse = await sendNodeHttpClientRequest(request);
while (this.handleRedirects && 300 <= response.statusCode && response.statusCode < 400 && response.headers.contains("location")) {
request.url = response.headers.get("location")!;
response = await sendNodeHttpClientRequest(request);
}
return response;
}
}
/**
* A fake HttpClient that can registered pre-determined HttpResponses for HttpRequests.
*/
export class FakeHttpClient implements HttpClient {
private readonly fakeResponses: HttpResponse[] = [];
public add(requestMethod: HttpMethod, requestUrl: string | URLBuilder, responseStatusCode?: number, responseHeaders?: HttpHeaders, responseBody?: string): FakeHttpClient {
this.fakeResponses.push({
request: {
method: requestMethod,
url: requestUrl
},
statusCode: responseStatusCode || 200,
headers: responseHeaders || new HttpHeaders(),
body: responseBody,
});
return this;
}
public sendRequest(request: HttpRequest): Promise<HttpResponse> {
let result: HttpResponse | undefined = last(this.fakeResponses, (fakeResponse: HttpResponse) => {
const fakeRequest: HttpRequest = fakeResponse.request;
return fakeRequest.method === request.method &&
fakeRequest.url.toString() === request.url.toString();
});
if (!result) {
result = {
request,
statusCode: 404,
headers: new HttpHeaders(),
};
}
return Promise.resolve(result);
}
}