Skip to content

Commit

Permalink
Merge pull request Azure#84 from RikkiGibson/HttpClient
Browse files Browse the repository at this point in the history
Don't expose fetch types in HttpOperationResponse
  • Loading branch information
RikkiGibson authored May 15, 2018
2 parents 9a15679 + 64b95ed commit f614aef
Show file tree
Hide file tree
Showing 11 changed files with 255 additions and 95 deletions.
30 changes: 25 additions & 5 deletions lib/fetchHttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { HttpClient } from "./httpClient";
import { HttpOperationResponse } from "./httpOperationResponse";
import { WebResource } from "./webResource";
import { RestError } from "./restError";
import { HttpHeaders } from "./httpHeaders";
import { isNode } from "./util/utils";

/**
* A HttpClient implementation that uses fetch to send HTTP requests.
Expand Down Expand Up @@ -47,8 +49,13 @@ export class FetchHttpClient implements HttpClient {
httpRequest.body = requestForm;
httpRequest.formData = undefined;
if (httpRequest.headers && httpRequest.headers["Content-Type"] &&
httpRequest.headers["Content-Type"].indexOf("multipart/form-data") > -1 && typeof requestForm.getBoundary === "function") {
httpRequest.headers["Content-Type"] = `multipart/form-data; boundary=${requestForm.getBoundary()}`;
httpRequest.headers["Content-Type"].indexOf("multipart/form-data") !== -1) {
if (typeof requestForm.getBoundary === "function") {
httpRequest.headers["Content-Type"] = `multipart/form-data; boundary=${requestForm.getBoundary()}`;
} else {
// browser will automatically apply a suitable content-type header
delete httpRequest.headers["Content-Type"];
}
}
}

Expand All @@ -62,14 +69,27 @@ export class FetchHttpClient implements HttpClient {
return Promise.reject(err);
}

const operationResponse = new HttpOperationResponse(httpRequest, res);

const headers = new HttpHeaders();
res.headers.forEach((value: string, name: string) => {
headers.set(name, value);
});

const operationResponse: HttpOperationResponse = {
request: httpRequest,
status: res.status,
headers,
readableStreamBody: isNode ? res.body as any : undefined,
blobBody: isNode ? undefined : () => res.blob()
};

if (!httpRequest.rawResponse) {
try {
operationResponse.bodyAsText = await res.text();
} catch (err) {
const msg = `Error "${err}" occured while converting the raw response body into string.`;
const errCode = err.code || "RAWTEXT_CONVERSION_ERROR";
const e = new RestError(msg, errCode, res.status, httpRequest, res, res.body);
const e = new RestError(msg, errCode, res.status, httpRequest, operationResponse, res.body);
return Promise.reject(e);
}

Expand All @@ -96,7 +116,7 @@ export class FetchHttpClient implements HttpClient {
} catch (err) {
const msg = `Error "${err}" occured while executing JSON.parse on the response body - ${operationResponse.bodyAsText}.`;
const errCode = err.code || "JSON_PARSE_ERROR";
const e = new RestError(msg, errCode, res.status, httpRequest, res, operationResponse.bodyAsText);
const e = new RestError(msg, errCode, res.status, httpRequest, operationResponse, operationResponse.bodyAsText);
return Promise.reject(e);
}
}
Expand Down
151 changes: 151 additions & 0 deletions lib/httpHeaders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

/**
* A collection of HttpHeaders that can be sent with a HTTP request.
*/
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] = 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 {
const result: RawHttpHeaders = {};

const headers: HttpHeader[] = this.headersArray();
for (let i = 0; i < headers.length; ++i) {
result[headers[i].name] = headers[i].value;
}

return result;
}

/**
* Create a deep clone/copy of this HttpHeaders collection.
*/
public clone(): HttpHeaders {
return new HttpHeaders(this.rawHeaders());
}
}
47 changes: 24 additions & 23 deletions lib/httpOperationResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License. See License.txt in the project root for license information.

import { WebResource } from "./webResource";
import { HttpHeaders } from "./httpHeaders";

/**
* Wrapper object for http request and response. Deserialized object is stored in
Expand All @@ -10,15 +11,22 @@ import { WebResource } from "./webResource";
* Initializes a new instance of the HttpOperationResponse class.
* @constructor
*/
export class HttpOperationResponse {
export interface HttpOperationResponse {
/**
* The raw request
*/
request: WebResource;

/**
* The HTTP response status (e.g. 200)
*/
status: number;

/**
* The raw response. Please use the response directly when the response body is a ReadableStream.
* The HTTP response headers.
*/
response: Response;
headers: HttpHeaders;

/**
* The response body as text (string format)
*/
Expand All @@ -27,24 +35,17 @@ export class HttpOperationResponse {
/**
* The response body as parsed JSON or XML
*/
parsedBody?: { [key: string]: any } | Array<any> | string | number | boolean | null | void;

constructor(request: WebResource, response: Response) {
/**
* Reference to the original request object.
* [WebResource] object.
* @type {object}
*/
this.request = request;

/**
* Reference to the original response object.
* [ServerResponse] object.
* @type {object}
*/
this.response = response;
/* tslint:disable:no-null-keyword */
this.bodyAsText = null;
this.parsedBody = null;
}
parsedBody?: any;

/**
* The response body as a Blob.
* Always undefined in node.js.
*/
blobBody?: (() => Promise<Blob>);

/**
* The response body as a node.js Readable stream.
* Always undefined in the browser.
*/
readableStreamBody?: NodeJS.ReadableStream;
}
59 changes: 24 additions & 35 deletions lib/msRest.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,39 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

import { WebResource, RequestPrepareOptions, HttpMethods, ParameterValue, RequestOptionsBase } from "./webResource";
import { FetchHttpClient } from "./fetchHttpClient";
import { HttpClient } from "./httpClient";
import { HttpOperationResponse } from "./httpOperationResponse";
import { HttpPipelineLogger } from "./httpPipelineLogger";
import { HttpPipelineLogLevel } from "./httpPipelineLogLevel";
export { WebResource, RequestPrepareOptions, HttpMethods, ParameterValue, RequestOptionsBase } from "./webResource";
export { FetchHttpClient } from "./fetchHttpClient";
export { HttpClient } from "./httpClient";
export { HttpOperationResponse } from "./httpOperationResponse";
export { HttpPipelineLogger } from "./httpPipelineLogger";
export { HttpPipelineLogLevel } from "./httpPipelineLogLevel";
export { RestError } from "./restError";
export { OperationSpec } from "./operationSpec";
import { RestError } from "./restError";
import { ServiceClient, ServiceClientOptions } from "./serviceClient";
import { Constants } from "./util/constants";
import { logPolicy } from "./policies/logPolicy";
import { BaseRequestPolicy, RequestPolicy } from "./policies/requestPolicy";
import { exponentialRetryPolicy } from "./policies/exponentialRetryPolicy";
import { systemErrorRetryPolicy } from "./policies/systemErrorRetryPolicy";
import { redirectPolicy } from "./policies/redirectPolicy";
import { signingPolicy } from "./policies/signingPolicy";
import { msRestUserAgentPolicy } from "./policies/msRestUserAgentPolicy";
import {
export { ServiceClient, ServiceClientOptions } from "./serviceClient";
export { Constants } from "./util/constants";
export { logPolicy } from "./policies/logPolicy";
export { BaseRequestPolicy, RequestPolicy } from "./policies/requestPolicy";
export { exponentialRetryPolicy } from "./policies/exponentialRetryPolicy";
export { systemErrorRetryPolicy } from "./policies/systemErrorRetryPolicy";
export { redirectPolicy } from "./policies/redirectPolicy";
export { signingPolicy } from "./policies/signingPolicy";
export { msRestUserAgentPolicy } from "./policies/msRestUserAgentPolicy";
export {
BaseMapperType, CompositeMapper, DictionaryMapper, EnumMapper, Mapper,
MapperConstraints, MapperType, PolymorphicDiscriminator,
SequenceMapper, Serializer, UrlParameterValue, serializeObject
} from "./serializer";
import {
export {
stripRequest, stripResponse, delay,
executePromisesSequentially, generateUuid, encodeUri, ServiceCallback,
promiseToCallback, promiseToServiceCallback, isValidUuid,
applyMixins, isNode, stringifyXML, prepareXMLRootList, isDuration
} from "./util/utils";
import { URLBuilder, URLQuery } from "./url";
export { URLBuilder, URLQuery } from "./url";

// Credentials
import { TokenCredentials } from "./credentials/tokenCredentials";
import { BasicAuthenticationCredentials } from "./credentials/basicAuthenticationCredentials";
import { ApiKeyCredentials, ApiKeyCredentialOptions } from "./credentials/apiKeyCredentials";
import { ServiceClientCredentials } from "./credentials/serviceClientCredentials";
import * as isStream from "is-stream";

export {
BaseMapperType, CompositeMapper, DictionaryMapper, EnumMapper, Mapper, MapperConstraints, MapperType, FetchHttpClient,
PolymorphicDiscriminator, SequenceMapper, UrlParameterValue, Serializer, serializeObject, HttpClient, HttpPipelineLogger, HttpPipelineLogLevel, TokenCredentials,
WebResource, RequestPrepareOptions, HttpMethods, ParameterValue, HttpOperationResponse, ServiceClient, Constants,
BasicAuthenticationCredentials, ApiKeyCredentials, ApiKeyCredentialOptions, ServiceClientCredentials, BaseRequestPolicy, logPolicy, ServiceClientOptions, exponentialRetryPolicy,
systemErrorRetryPolicy, signingPolicy, msRestUserAgentPolicy, stripRequest, stripResponse, delay, executePromisesSequentially,
generateUuid, isValidUuid, encodeUri, RestError, RequestOptionsBase, RequestPolicy, ServiceCallback, promiseToCallback,
promiseToServiceCallback, isStream, redirectPolicy, applyMixins, isNode, stringifyXML, prepareXMLRootList, isDuration,
URLBuilder, URLQuery
};
export { TokenCredentials } from "./credentials/tokenCredentials";
export { BasicAuthenticationCredentials } from "./credentials/basicAuthenticationCredentials";
export { ApiKeyCredentials, ApiKeyCredentialOptions } from "./credentials/apiKeyCredentials";
export { ServiceClientCredentials } from "./credentials/serviceClientCredentials";
export const isStream = require("is-stream");
11 changes: 5 additions & 6 deletions lib/policies/exponentialRetryPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,25 +111,24 @@ export class ExponentialRetryPolicy extends BaseRequestPolicy {
return retryData;
}

async retry(operationResponse: HttpOperationResponse, retryData?: RetryData, err?: RetryError): Promise<HttpOperationResponse> {
async retry(response: HttpOperationResponse, retryData?: RetryData, err?: RetryError): Promise<HttpOperationResponse> {
const self = this;
const response = operationResponse.response;
retryData = self.updateRetryData(retryData, err);
if (!utils.objectIsNull(response) && self.shouldRetry(response.status, retryData)) {
if (self.shouldRetry(response.status, retryData)) {
try {
await utils.delay(retryData.retryInterval);
const res: HttpOperationResponse = await this._nextPolicy.sendRequest(operationResponse.request);
const res: HttpOperationResponse = await this._nextPolicy.sendRequest(response.request);
return self.retry(res, retryData, err);
} catch (err) {
return self.retry(operationResponse, retryData, err);
return self.retry(response, retryData, err);
}
} else {
if (!utils.objectIsNull(err)) {
// If the operation failed in the end, return all errors instead of just the last one
err = retryData.error;
return Promise.reject(err);
}
return Promise.resolve(operationResponse);
return Promise.resolve(response);
}
}

Expand Down
Loading

0 comments on commit f614aef

Please sign in to comment.