diff --git a/src/script/acquisition-sdk.ts b/src/script/acquisition-sdk.ts index 1feb5b3f..b916f004 100644 --- a/src/script/acquisition-sdk.ts +++ b/src/script/acquisition-sdk.ts @@ -58,6 +58,7 @@ export class AcquisitionStatus { } export class AcquisitionManager { + private readonly BASE_URL_PART = "appcenter.ms"; private _appVersion: string; private _clientUniqueId: string; private _deploymentKey: string; @@ -65,7 +66,8 @@ export class AcquisitionManager { private _ignoreAppVersion: boolean; private _serverUrl: string; private _publicPrefixUrl: string = "v0.1/public/codepush/"; - + private _statusCode: number; + private static _apiCallsDisabled: boolean = false; constructor(httpRequester: Http.Requester, configuration: Configuration) { this._httpRequester = httpRequester; @@ -80,7 +82,21 @@ export class AcquisitionManager { this._ignoreAppVersion = configuration.ignoreAppVersion; } + private isRecoverable = (statusCode: number): boolean => statusCode >= 500 || statusCode === 408 || statusCode === 429; + + private handleRequestFailure() { + if (this._serverUrl.includes(this.BASE_URL_PART) && !this.isRecoverable(this._statusCode)) { + AcquisitionManager._apiCallsDisabled = true; + } + } + public queryUpdateWithCurrentPackage(currentPackage: Package, callback?: Callback): void { + if (AcquisitionManager._apiCallsDisabled) { + console.log(`[CodePush] Api calls are disabled, skipping API call`); + callback(/*error=*/ null, /*remotePackage=*/ null); + return; + } + if (!currentPackage || !currentPackage.appVersion) { throw new CodePushPackageError("Calling common acquisition SDK with incorrect package"); // Unexpected; indicates error in our implementation } @@ -102,8 +118,10 @@ export class AcquisitionManager { return; } - if (response.statusCode !== 200) { + if (response.statusCode < 200 || response.statusCode >= 300) { let errorMessage: any; + this._statusCode = response.statusCode; + this.handleRequestFailure(); if (response.statusCode === 0) { errorMessage = `Couldn't send request to ${requestUrl}, xhr.statusCode = 0 was returned. One of the possible reasons for that might be connection problems. Please, check your internet connection.`; } else { @@ -147,6 +165,12 @@ export class AcquisitionManager { } public reportStatusDeploy(deployedPackage?: Package, status?: string, previousLabelOrAppVersion?: string, previousDeploymentKey?: string, callback?: Callback): void { + if (AcquisitionManager._apiCallsDisabled) { + console.log(`[CodePush] Api calls are disabled, skipping API call`); + callback(/*error*/ null, /*not used*/ null); + return; + } + var url: string = this._serverUrl + this._publicPrefixUrl + "report_status/deploy"; var body: DeploymentStatusReport = { app_version: this._appVersion, @@ -196,7 +220,9 @@ export class AcquisitionManager { return; } - if (response.statusCode !== 200) { + if (response.statusCode < 200 || response.statusCode >= 300) { + this._statusCode = response.statusCode; + this.handleRequestFailure(); callback(new CodePushHttpError(response.statusCode + ": " + response.body), /*not used*/ null); return; } @@ -207,6 +233,12 @@ export class AcquisitionManager { } public reportStatusDownload(downloadedPackage: Package, callback?: Callback): void { + if (AcquisitionManager._apiCallsDisabled) { + console.log(`[CodePush] Api calls are disabled, skipping API call`); + callback(/*error*/ null, /*not used*/ null); + return; + } + var url: string = this._serverUrl + this._publicPrefixUrl + "report_status/download"; var body: DownloadReport = { client_unique_id: this._clientUniqueId, @@ -221,7 +253,9 @@ export class AcquisitionManager { return; } - if (response.statusCode !== 200) { + if (response.statusCode < 200 || response.statusCode >= 300) { + this._statusCode = response.statusCode; + this.handleRequestFailure(); callback(new CodePushHttpError(response.statusCode + ": " + response.body), /*not used*/ null); return; } diff --git a/src/test/acquisition-rest-mock.ts b/src/test/acquisition-rest-mock.ts index 5b179a91..b3620d83 100644 --- a/src/test/acquisition-rest-mock.ts +++ b/src/test/acquisition-rest-mock.ts @@ -22,7 +22,19 @@ var reportStatusDeployUrl = serverUrl + publicPrefixUrl + "/report_status/deploy var reportStatusDownloadUrl = serverUrl + publicPrefixUrl + "/report_status/download"; var updateCheckUrl = serverUrl + publicPrefixUrl + "/update_check?"; +export function updateMockUrl() { + reportStatusDeployUrl = serverUrl + publicPrefixUrl + "/report_status/deploy"; + reportStatusDownloadUrl = serverUrl + publicPrefixUrl + "/report_status/download"; + updateCheckUrl = serverUrl + publicPrefixUrl + "/update_check?"; +} + export class HttpRequester implements acquisitionSdk.Http.Requester { + private expectedStatusCode: number; + + constructor(expectedStatusCode?: number) { + this.expectedStatusCode = expectedStatusCode; + } + public request(verb: acquisitionSdk.Http.Verb, url: string, requestBodyOrCallback: string | acquisitionSdk.Callback, callback?: acquisitionSdk.Callback): void { if (!callback && typeof requestBodyOrCallback === "function") { callback = >requestBodyOrCallback; @@ -30,11 +42,11 @@ export class HttpRequester implements acquisitionSdk.Http.Requester { if (verb === acquisitionSdk.Http.Verb.GET && url.indexOf(updateCheckUrl) === 0) { var params = querystring.parse(url.substring(updateCheckUrl.length)); - Server.onUpdateCheck(params, callback); + Server.onUpdateCheck(params, callback, this.expectedStatusCode); } else if (verb === acquisitionSdk.Http.Verb.POST && url === reportStatusDeployUrl) { - Server.onReportStatus(callback); + Server.onReportStatus(callback, this.expectedStatusCode); } else if (verb === acquisitionSdk.Http.Verb.POST && url === reportStatusDownloadUrl) { - Server.onReportStatus(callback); + Server.onReportStatus(callback, this.expectedStatusCode); } else { throw new Error("Unexpected call"); } @@ -73,7 +85,7 @@ class Server { } } - public static onUpdateCheck(params: any, callback: acquisitionSdk.Callback): void { + public static onUpdateCheck(params: any, callback: acquisitionSdk.Callback, expectedStatusCode?: number): void { var updateRequest: types.UpdateCheckRequest = { deployment_key: params.deployment_key, app_version: params.app_version, @@ -97,13 +109,13 @@ class Server { } callback(/*error=*/ null, { - statusCode: 200, + statusCode: expectedStatusCode ? expectedStatusCode : 200, body: JSON.stringify({ update_info: updateInfo }) }); } } - public static onReportStatus(callback: acquisitionSdk.Callback): void { - callback(/*error*/ null, /*response*/ { statusCode: 200 }); + public static onReportStatus(callback: acquisitionSdk.Callback, expectedStatusCode: number): void { + callback(/*error*/ null, /*response*/ { statusCode: expectedStatusCode ? expectedStatusCode : 200 }); } } diff --git a/src/test/acquisition-sdk.ts b/src/test/acquisition-sdk.ts index 2bafcd48..18aa86f8 100644 --- a/src/test/acquisition-sdk.ts +++ b/src/test/acquisition-sdk.ts @@ -4,6 +4,7 @@ import * as acquisitionSdk from "../script/acquisition-sdk"; import * as acquisitionRestMock from "./acquisition-rest-mock"; import * as types from "../script/types"; import { CodePushPackageError } from "../script/code-push-error" +import { updateMockUrl } from "./acquisition-rest-mock"; const mockApi = acquisitionRestMock; var latestPackage: types.UpdateCheckResponse = clone(mockApi.latestPackage); @@ -44,6 +45,8 @@ var nativeUpdateResult: acquisitionSdk.NativeUpdateNotification = { describe("Acquisition SDK", () => { beforeEach(() => { mockApi.latestPackage = clone(latestPackage); + mockApi.serverUrl = "http://myurl.com"; + updateMockUrl(); }); it("Package with lower label and different package hash gives update", (done: Mocha.Done) => { @@ -226,6 +229,68 @@ describe("Acquisition SDK", () => { done(); })); }); + + it("disables api calls on unsuccessful response", (done: Mocha.Done): void => { + var invalidJsonResponse: acquisitionSdk.Http.Response = { + statusCode: 404, + body: "Not found" + }; + + mockApi.serverUrl = "https://codepush.appcenter.ms"; + updateMockUrl(); + configuration = { ...configuration, serverUrl: "https://codepush.appcenter.ms" }; + + var acquisition = new acquisitionSdk.AcquisitionManager(new mockApi.CustomResponseHttpRequester(invalidJsonResponse), configuration); + acquisition.queryUpdateWithCurrentPackage(templateCurrentPackage, (error: Error, returnPackage: acquisitionSdk.RemotePackage | acquisitionSdk.NativeUpdateNotification) => { + assert.strictEqual((acquisitionSdk.AcquisitionManager as any)._apiCallsDisabled, true); + (acquisitionSdk.AcquisitionManager as any)._apiCallsDisabled = false; + }); + + acquisition.queryUpdateWithCurrentPackage(templateCurrentPackage, (error: Error, returnPackage: acquisitionSdk.RemotePackage | acquisitionSdk.NativeUpdateNotification) => { + assert.strictEqual(returnPackage, null); + acquisition = new acquisitionSdk.AcquisitionManager(new mockApi.HttpRequester(404), configuration); + (acquisitionSdk.AcquisitionManager as any)._apiCallsDisabled = false; + }); + + acquisition.reportStatusDeploy(templateCurrentPackage, acquisitionSdk.AcquisitionStatus.DeploymentSucceeded, "1.5.0", mockApi.validDeploymentKey, ((error: Error, parameter: void): void => { + assert.strictEqual((acquisitionSdk.AcquisitionManager as any)._apiCallsDisabled, true); + acquisition = new acquisitionSdk.AcquisitionManager(new mockApi.HttpRequester(404), configuration); + (acquisitionSdk.AcquisitionManager as any)._apiCallsDisabled = false; + })); + + acquisition.reportStatusDownload(templateCurrentPackage, ((error: Error, parameter: void): void => { + assert.strictEqual((acquisitionSdk.AcquisitionManager as any)._apiCallsDisabled, true); + acquisition = acquisition = new acquisitionSdk.AcquisitionManager(new mockApi.CustomResponseHttpRequester(invalidJsonResponse), configuration); + (acquisitionSdk.AcquisitionManager as any)._apiCallsDisabled = false; + })); + + done(); + }) + + it("doesn't disable api calls on successful response", (done: Mocha.Done): void => { + var acquisition = new acquisitionSdk.AcquisitionManager(new mockApi.HttpRequester(), configuration); + mockApi.serverUrl = "https://codepush.appcenter.ms"; + updateMockUrl(); + + acquisition.queryUpdateWithCurrentPackage(templateCurrentPackage, (error: Error, returnPackage: acquisitionSdk.RemotePackage | acquisitionSdk.NativeUpdateNotification) => { + assert.strictEqual((acquisitionSdk.AcquisitionManager as any)._apiCallsDisabled, false); + }); + + acquisition.queryUpdateWithCurrentPackage(templateCurrentPackage, (error: Error, returnPackage: acquisitionSdk.RemotePackage | acquisitionSdk.NativeUpdateNotification) => { + assert.notStrictEqual(returnPackage, null); + }); + + acquisition.reportStatusDeploy(templateCurrentPackage, acquisitionSdk.AcquisitionStatus.DeploymentSucceeded, "1.5.0", mockApi.validDeploymentKey, ((error: Error, parameter: void): void => { + assert.strictEqual((acquisitionSdk.AcquisitionManager as any)._apiCallsDisabled, false); + })); + + acquisition.reportStatusDownload(templateCurrentPackage, ((error: Error, parameter: void): void => { + assert.strictEqual((acquisitionSdk.AcquisitionManager as any)._apiCallsDisabled, false); + })); + + done(); + }) + }); function clone(initialObject: T): T {