diff --git a/lib/axiosHttpClient.ts b/lib/axiosHttpClient.ts index 648b31236de2..95b6b1287143 100644 --- a/lib/axiosHttpClient.ts +++ b/lib/axiosHttpClient.ts @@ -80,7 +80,7 @@ export class AxiosHttpClient implements HttpClient { const abortSignal = httpRequest.abortSignal; if (abortSignal && abortSignal.aborted) { - throw new RestError("The request was aborted", "REQUEST_ABORTED_ERROR", undefined, httpRequest); + throw new RestError("The request was aborted", RestError.REQUEST_ABORTED_ERROR, undefined, httpRequest); } let abortListener: (() => void) | undefined; @@ -136,10 +136,10 @@ export class AxiosHttpClient implements HttpClient { res = await axiosClient(config); } catch (err) { if (err instanceof axios.Cancel) { - throw new RestError(err.message, "REQUEST_ABORTED_ERROR", undefined, httpRequest); + throw new RestError(err.message, RestError.REQUEST_SEND_ERROR, undefined, httpRequest); } else { const axiosErr = err as AxiosError; - throw new RestError(axiosErr.message, "REQUEST_SEND_ERROR", undefined, httpRequest); + throw new RestError(axiosErr.message, RestError.REQUEST_SEND_ERROR, undefined, httpRequest); } } finally { if (abortSignal && abortListener) { diff --git a/lib/httpOperationResponse.ts b/lib/httpOperationResponse.ts index ca19aa408daf..221ee6a4f21b 100644 --- a/lib/httpOperationResponse.ts +++ b/lib/httpOperationResponse.ts @@ -58,7 +58,7 @@ export interface HttpOperationResponse extends HttpResponse { * The response body as a browser Blob. * Always undefined in node.js. */ - blobBody?: () => Promise; + blobBody?: Promise; /** * NODEJS ONLY @@ -68,3 +68,17 @@ export interface HttpOperationResponse extends HttpResponse { */ readableStreamBody?: NodeJS.ReadableStream; } + +/** + * The flattened response to a REST call. + * Contains the underlying HttpOperationResponse as well as + * the merged properties of the parsedBody, parsedHeaders, etc. + */ +export interface RestResponse { + /** + * The underlying HTTP response containing both raw and deserialized response data. + */ + _response: HttpOperationResponse; + + [key: string]: any; +} diff --git a/lib/msRest.ts b/lib/msRest.ts index 0c67d4fc369b..42c40e8b54fd 100644 --- a/lib/msRest.ts +++ b/lib/msRest.ts @@ -5,7 +5,7 @@ export { WebResource, HttpRequestBody, RequestPrepareOptions, HttpMethods, Param export { DefaultHttpClient } from "./defaultHttpClient"; export { HttpClient } from "./httpClient"; export { HttpHeaders } from "./httpHeaders"; -export { HttpOperationResponse, HttpResponse } from "./httpOperationResponse"; +export { HttpOperationResponse, HttpResponse, RestResponse } from "./httpOperationResponse"; export { HttpPipelineLogger } from "./httpPipelineLogger"; export { HttpPipelineLogLevel } from "./httpPipelineLogLevel"; export { RestError } from "./restError"; @@ -13,7 +13,7 @@ export { OperationArguments } from "./operationArguments"; export { OperationParameter, OperationQueryParameter, OperationURLParameter } from "./operationParameter"; export { OperationResponse } from "./operationResponse"; export { OperationSpec } from "./operationSpec"; -export { ServiceClient, ServiceClientOptions } from "./serviceClient"; +export { ServiceClient, ServiceClientOptions, flattenResponse } from "./serviceClient"; export { QueryCollectionFormat } from "./queryCollectionFormat"; export { Constants } from "./util/constants"; export { logPolicy } from "./policies/logPolicy"; @@ -33,7 +33,7 @@ export { export { stripRequest, stripResponse, delay, executePromisesSequentially, generateUuid, encodeUri, ServiceCallback, - promiseToCallback, responseToBody, promiseToServiceCallback, isValidUuid, + promiseToCallback, promiseToServiceCallback, isValidUuid, applyMixins, isNode, isDuration } from "./util/utils"; export { URLBuilder, URLQuery } from "./url"; diff --git a/lib/policies/deserializationPolicy.ts b/lib/policies/deserializationPolicy.ts index 12a6fa77de3d..b8bde31aa389 100644 --- a/lib/policies/deserializationPolicy.ts +++ b/lib/policies/deserializationPolicy.ts @@ -147,6 +147,9 @@ export function deserializeResponseBody(response: HttpOperationResponse): Promis restError.response = utils.stripResponse(parsedResponse); return Promise.reject(restError); } + } else if (operationSpec.httpMethod === "HEAD") { + // head methods never have a body, but we return a boolean to indicate presence/absence of the resource + parsedResponse.parsedBody = response.status >= 200 && response.status < 300; } if (responseSpec.headersMapper) { @@ -160,9 +163,9 @@ export function deserializeResponseBody(response: HttpOperationResponse): Promis } function parse(operationResponse: HttpOperationResponse): Promise { - const errorHandler = (err: any) => { + const errorHandler = (err: Error & { code: string }) => { const msg = `Error "${err}" occurred while parsing the response body - ${operationResponse.bodyAsText}.`; - const errCode = err.code || "PARSE_ERROR"; + const errCode = err.code || RestError.PARSE_ERROR; const e = new RestError(msg, errCode, operationResponse.status, operationResponse.request, operationResponse, operationResponse.bodyAsText); return Promise.reject(e); }; diff --git a/lib/policies/exponentialRetryPolicy.ts b/lib/policies/exponentialRetryPolicy.ts index b49b5a2391f5..7c297d1ba232 100644 --- a/lib/policies/exponentialRetryPolicy.ts +++ b/lib/policies/exponentialRetryPolicy.ts @@ -5,6 +5,7 @@ import { HttpOperationResponse } from "../httpOperationResponse"; import * as utils from "../util/utils"; import { WebResource } from "../webResource"; import { BaseRequestPolicy, RequestPolicy, RequestPolicyFactory, RequestPolicyOptions } from "./requestPolicy"; +import { RestError } from "../restError"; export interface RetryData { retryCount: number; @@ -86,8 +87,8 @@ export class ExponentialRetryPolicy extends BaseRequestPolicy { * @param {RetryData} retryData The retry data. * @return {boolean} True if the operation qualifies for a retry; false otherwise. */ -function shouldRetry(policy: ExponentialRetryPolicy, statusCode: number, retryData: RetryData): boolean { - if ((statusCode < 500 && statusCode !== 408) || statusCode === 501 || statusCode === 505) { +function shouldRetry(policy: ExponentialRetryPolicy, statusCode: number | undefined, retryData: RetryData): boolean { + if (statusCode == undefined || (statusCode < 500 && statusCode !== 408) || statusCode === 501 || statusCode === 505) { return false; } @@ -138,18 +139,24 @@ function updateRetryData(policy: ExponentialRetryPolicy, retryData?: RetryData, return retryData; } -function retry(policy: ExponentialRetryPolicy, request: WebResource, response: HttpOperationResponse, retryData?: RetryData, requestError?: RetryError): Promise { +function retry(policy: ExponentialRetryPolicy, request: WebResource, response?: HttpOperationResponse, retryData?: RetryData, requestError?: RetryError): Promise { retryData = updateRetryData(policy, retryData, requestError); const isAborted: boolean | undefined = request.abortSignal && request.abortSignal.aborted; - if (!isAborted && shouldRetry(policy, response.status, retryData)) { + if (!isAborted && shouldRetry(policy, response && response.status, retryData)) { return utils.delay(retryData.retryInterval) .then(() => policy._nextPolicy.sendRequest(request.clone())) .then(res => retry(policy, request, res, retryData, undefined)) .catch(err => retry(policy, request, response, retryData, err)); - } else if (isAborted || requestError != undefined) { + } else if (isAborted || requestError || !response) { // If the operation failed in the end, return all errors instead of just the last one - requestError = retryData.error; - return Promise.reject(requestError); + const err = retryData.error || + new RestError( + "Failed to send the request.", + RestError.REQUEST_SEND_ERROR, + response && response.status, + response && response.request, + response); + return Promise.reject(err); } else { return Promise.resolve(response); } diff --git a/lib/restError.ts b/lib/restError.ts index 465c212f36bb..e31272037a93 100644 --- a/lib/restError.ts +++ b/lib/restError.ts @@ -5,6 +5,10 @@ import { HttpOperationResponse } from "./httpOperationResponse"; import { WebResource } from "./webResource"; export class RestError extends Error { + static readonly REQUEST_SEND_ERROR = "REQUEST_SEND_ERROR"; + static readonly REQUEST_ABORTED_ERROR = "REQUEST_ABORTED_ERROR"; + static readonly PARSE_ERROR = "PARSE_ERROR"; + code?: string; statusCode?: number; request?: WebResource; diff --git a/lib/serviceClient.ts b/lib/serviceClient.ts index 6931a4cbd1da..3483a474e725 100644 --- a/lib/serviceClient.ts +++ b/lib/serviceClient.ts @@ -4,7 +4,7 @@ import { ServiceClientCredentials } from "./credentials/serviceClientCredentials"; import { DefaultHttpClient } from "./defaultHttpClient"; import { HttpClient } from "./httpClient"; -import { HttpOperationResponse } from "./httpOperationResponse"; +import { HttpOperationResponse, RestResponse } from "./httpOperationResponse"; import { HttpPipelineLogger } from "./httpPipelineLogger"; import { OperationArguments } from "./operationArguments"; import { getPathStringFromParameter, getPathStringFromParameterPath, OperationParameter, ParameterPath } from "./operationParameter"; @@ -25,6 +25,8 @@ import { Constants } from "./util/constants"; import * as utils from "./util/utils"; import { stringifyXML } from "./util/xml"; import { RequestOptionsBase, RequestPrepareOptions, WebResource } from "./webResource"; +import { OperationResponse } from "./operationResponse"; +import { ServiceCallback } from "./util/utils"; /** * Options to be provided while creating the client. @@ -176,11 +178,17 @@ export class ServiceClient { * Send an HTTP request that is populated using the provided OperationSpec. * @param {OperationArguments} operationArguments The arguments that the HTTP request's templated values will be populated from. * @param {OperationSpec} operationSpec The OperationSpec to use to populate the httpRequest. + * @param {ServiceCallback} callback The callback to call when the response is received. */ - sendOperationRequest(operationArguments: OperationArguments, operationSpec: OperationSpec): Promise { + sendOperationRequest(operationArguments: OperationArguments, operationSpec: OperationSpec, callback?: ServiceCallback): Promise { + if (typeof operationArguments.options === "function") { + callback = operationArguments.options; + operationArguments.options = undefined; + } + const httpRequest = new WebResource(); - let result: Promise; + let result: Promise; try { const baseUri: string | undefined = operationSpec.baseUrl || this.baseUri; if (!baseUri) { @@ -294,10 +302,20 @@ export class ServiceClient { httpRequest.streamResponseBody = isStreamOperation(operationSpec); } - result = this.sendRequest(httpRequest); + result = this.sendRequest(httpRequest) + .then(res => flattenResponse(res, operationSpec.responses[res.status])); } catch (error) { result = Promise.reject(error); } + + const cb = callback; + if (cb) { + result + // tslint:disable-next-line:no-null-keyword + .then(res => cb(null, res._response.parsedBody, res._response.request, res._response)) + .catch(err => cb(err)); + } + return result; } } @@ -460,3 +478,53 @@ function getPropertyFromParameterPath(parent: { [parameterName: string]: any }, } return result; } + +export function flattenResponse(_response: HttpOperationResponse, responseSpec: OperationResponse | undefined): RestResponse { + const parsedHeaders = _response.parsedHeaders; + const bodyMapper = responseSpec && responseSpec.bodyMapper; + if (bodyMapper) { + const typeName = bodyMapper.type.name; + if (typeName === "Stream") { + return { + ...parsedHeaders, + blobBody: _response.blobBody, + readableStreamBody: _response.readableStreamBody, + _response + }; + } + + if (typeName === "Sequence") { + const arrayResponse = [...(_response.parsedBody) || []] as RestResponse & any[]; + if (parsedHeaders) { + for (const key of Object.keys(parsedHeaders)) { + arrayResponse[key] = parsedHeaders[key]; + } + } + arrayResponse._response = _response; + return arrayResponse; + } + + if (typeName === "Composite" || typeName === "Dictionary") { + return { + ...parsedHeaders, + ..._response.parsedBody, + _response + }; + } + } + + if (bodyMapper || _response.request.method === "HEAD") { + // primitive body types and HEAD booleans + return { + ...parsedHeaders, + body: _response.parsedBody, + _response + }; + } + + return { + ...parsedHeaders, + ..._response.parsedBody, + _response + }; +} diff --git a/lib/util/utils.ts b/lib/util/utils.ts index c5d8ba752a59..377ca725640a 100644 --- a/lib/util/utils.ts +++ b/lib/util/utils.ts @@ -12,29 +12,6 @@ import { Constants } from "./constants"; */ export const isNode = typeof navigator === "undefined" && typeof process !== "undefined"; -/** - * Adapts a WithHttpOperationResponse method to unwrap the body and accept an optional callback. - * @param httpOperationResponseMethod the method to call and apply a callback to - * @param args the arguments to the method, optionally including a trailing callback. - */ -export function responseToBody(httpOperationResponseMethod: (...args: any[]) => Promise, ...args: any[]): Promise | undefined { - // The generated code will always pass (options, callback) as the last two args. - // But `options` could actually be the callback for the sake of user convenience. - let callback = args.pop(); - if (typeof callback !== "function" && typeof args[args.length - 1] === "function") { - callback = args.pop(); - } - if (typeof callback === "function") { - httpOperationResponseMethod(...args) - // tslint:disable-next-line:no-null-keyword - .then(res => callback(null, res.parsedBody, res.request, res)) - .catch(err => callback(err)); - } else { - return httpOperationResponseMethod(...args).then((res: HttpOperationResponse) => res.parsedBody); - } - return undefined; // optimized out -} - /** * Checks if a parsed URL is HTTPS * @@ -194,12 +171,12 @@ export function delay(t: number, value?: T): Promise { export interface ServiceCallback { /** * A method that will be invoked as a callback to a service function. - * @param {Error | RestError} err The error occurred if any, while executing the request; otherwise null. + * @param {Error | RestError | null} err The error occurred if any, while executing the request; otherwise null. * @param {TResult} [result] The deserialized response body if an error did not occur. * @param {WebResource} [request] The raw/actual request sent to the server if an error did not occur. * @param {HttpOperationResponse} [response] The raw/actual response from the server if an error did not occur. */ - (err: Error | RestError, result?: TResult, request?: WebResource, response?: HttpOperationResponse): void; + (err: Error | RestError | null, result?: TResult, request?: WebResource, response?: HttpOperationResponse): void; } /** diff --git a/lib/xhrHttpClient.ts b/lib/xhrHttpClient.ts index d32efc303c2e..ff5e3dcdde21 100644 --- a/lib/xhrHttpClient.ts +++ b/lib/xhrHttpClient.ts @@ -74,7 +74,7 @@ export class XhrHttpClient implements HttpClient { xhr.addEventListener("readystatechange", () => { // Resolve as soon as headers are loaded if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) { - const bodyPromise = new Promise((resolve, reject) => { + const blobBody = new Promise((resolve, reject) => { xhr.addEventListener("load", () => { resolve(xhr.response); }); @@ -84,7 +84,7 @@ export class XhrHttpClient implements HttpClient { request, status: xhr.status, headers: parseHeaders(xhr), - blobBody: () => bodyPromise + blobBody }); } }); @@ -127,7 +127,7 @@ export function parseHeaders(xhr: XMLHttpRequest) { } function rejectOnTerminalEvent(request: WebResource, xhr: XMLHttpRequest, reject: (err: any) => void) { - xhr.addEventListener("error", () => reject(new RestError(`Failed to send request to ${request.url}`, "REQUEST_SEND_ERROR", undefined, request))); - xhr.addEventListener("abort", () => reject(new RestError("The request was aborted", "REQUEST_ABORTED_ERROR", undefined, request))); - xhr.addEventListener("timeout", () => reject(new RestError(`timeout of ${xhr.timeout}ms exceeded`, "REQUEST_SEND_ERROR", undefined, request))); + xhr.addEventListener("error", () => reject(new RestError(`Failed to send request to ${request.url}`, RestError.REQUEST_SEND_ERROR, undefined, request))); + xhr.addEventListener("abort", () => reject(new RestError("The request was aborted", RestError.REQUEST_ABORTED_ERROR, undefined, request))); + xhr.addEventListener("timeout", () => reject(new RestError(`timeout of ${xhr.timeout}ms exceeded`, RestError.REQUEST_SEND_ERROR, undefined, request))); } diff --git a/package.json b/package.json index 5683e390bee5..56d375848c1d 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "email": "azsdkteam@microsoft.com", "url": "https://github.com/Azure/ms-rest-js" }, - "version": "0.20.0", + "version": "0.21.0", "description": "Isomorphic client Runtime for Typescript/node.js/browser javascript client libraries generated using AutoRest", "tags": [ "isomorphic", diff --git a/test/shared/defaultHttpClientTests.ts b/test/shared/defaultHttpClientTests.ts index 3b8387f4d870..3f246f360f24 100644 --- a/test/shared/defaultHttpClientTests.ts +++ b/test/shared/defaultHttpClientTests.ts @@ -210,7 +210,7 @@ describe("defaultHttpClient", () => { const response = await client.sendRequest(request); const streamBody = response.readableStreamBody; if (response.blobBody) { - await response.blobBody(); + await response.blobBody; } else if (streamBody) { streamBody.on('data', () => {}); await new Promise((resolve, reject) => { diff --git a/test/shared/serviceClientTests.ts b/test/shared/serviceClientTests.ts index 2d75c25ce944..1766f853b36f 100644 --- a/test/shared/serviceClientTests.ts +++ b/test/shared/serviceClientTests.ts @@ -1,11 +1,10 @@ import * as assert from "assert"; import { HttpClient } from "../../lib/httpClient"; -import { HttpOperationResponse } from "../../lib/httpOperationResponse"; import { QueryCollectionFormat } from "../../lib/queryCollectionFormat"; import { DictionaryMapper, MapperType, Serializer, Mapper } from "../../lib/serializer"; import { serializeRequestBody, ServiceClient, getOperationArgumentValueFromParameterPath } from "../../lib/serviceClient"; import { WebResource } from "../../lib/webResource"; -import { OperationArguments } from "../../lib/msRest"; +import { OperationArguments, HttpHeaders } from "../../lib/msRest"; import { ParameterPath } from "../../lib/operationParameter"; describe("ServiceClient", function () { @@ -22,7 +21,7 @@ describe("ServiceClient", function () { httpClient: { sendRequest: req => { request = req; - return Promise.resolve({} as HttpOperationResponse); + return Promise.resolve({ request, status: 200, headers: new HttpHeaders() }); } }, requestPolicyFactories: [] @@ -80,7 +79,7 @@ describe("ServiceClient", function () { httpClient: { sendRequest: req => { request = req; - return Promise.resolve({} as HttpOperationResponse); + return Promise.resolve({ request, status: 200, headers: new HttpHeaders() }); }, }, requestPolicyFactories: [], @@ -127,7 +126,7 @@ describe("ServiceClient", function () { const httpClient: HttpClient = { sendRequest: req => { request = req; - return Promise.resolve({} as HttpOperationResponse); + return Promise.resolve({ request, status: 200, headers: new HttpHeaders() }); } }; diff --git a/test/shared/utilsTests.ts b/test/shared/utilsTests.ts deleted file mode 100644 index 7ca70243f876..000000000000 --- a/test/shared/utilsTests.ts +++ /dev/null @@ -1,70 +0,0 @@ -import * as should from "should"; -import { responseToBody, HttpOperationResponse, HttpHeaders, WebResource } from "../../lib/msRest"; - -const response: HttpOperationResponse = { status: 200, headers: new HttpHeaders(), parsedBody: { foo: 42 }, request: new WebResource() }; - -describe("responseToBody", function() { - it("should pass arguments", function() { - let r1 = false; - let r2 = 0; - let r3: { [key: string]: string } = {}; - const responseMethod = (p1: boolean, p2: number, opts: {}) => { - r1 = p1; - r2 = p2; - r3 = opts; - return Promise.resolve(response); - }; - responseToBody(responseMethod, true, 123, { opt: "hello" }, undefined); - r1.should.equal(true); - r2.should.equal(123); - r3.opt.should.equal("hello"); - }); - - it("should return a promise when no callback is provided", function() { - const result = responseToBody(() => Promise.resolve(response)); - result!.should.be.instanceof(Promise); - }); - - it("should call the callback in the last argument", function(done) { - const response: HttpOperationResponse = { status: 200, headers: new HttpHeaders(), parsedBody: { foo: 42 }, request: new WebResource() }; - const responseMethod = () => { - return Promise.resolve(response); - }; - - const result = responseToBody(responseMethod, (err: any, body: any, request: WebResource, innerResponse: HttpOperationResponse) => { - should(err).equal(null); - body.foo.should.equal(42); - request.should.equal(response.request); - innerResponse.should.equal(response); - done(); - }); - should(result).equal(undefined); - }); - - it("should call the callback in the second to last argument", function(done) { - let response: HttpOperationResponse = { status: 200, headers: new HttpHeaders(), parsedBody: { foo: 42 }, request: new WebResource() }; - const responseMethod = () => { - return Promise.resolve(response); - }; - - const result = responseToBody(responseMethod, (err: any, body: any, request: WebResource, innerResponse: HttpOperationResponse) => { - should(err).equal(null); - body.foo.should.equal(42); - request.should.equal(response.request); - innerResponse.should.equal(response); - done(); - }, undefined); - should(result).equal(undefined); - }); - - it("should pass errors to the callback", function(done) { - const responseMethod = () => { - return Promise.reject(new Error()); - }; - - responseToBody(responseMethod, (err: Error) => { - err.should.be.instanceof(Error); - done(); - }); - }); -}); \ No newline at end of file