diff --git a/ui/src/Common/Fetch.js b/ui/src/Common/Fetch.js index fb09d84c1..625016fce 100644 --- a/ui/src/Common/Fetch.js +++ b/ui/src/Common/Fetch.js @@ -8,17 +8,29 @@ const CommonOptions = { }; const FetchRetryConfig = { - retries: 5, - minTimeout: 1000, + retries: 10, + minTimeout: 2000, maxTimeout: 5000 }; const FetchGet = async (uri, options, retryOptions) => await promiseRetry( (retry, number) => - fetch(uri, merge({}, { method: "GET" }, CommonOptions, options)).catch( - retry - ), + fetch( + uri, + merge( + {}, + { + method: "GET", + mode: + number <= Math.round(FetchRetryConfig.retries * 0.8) + ? "cors" + : "no-cors" + }, + CommonOptions, + options + ) + ).catch(retry), FetchRetryConfig ); diff --git a/ui/src/Common/Fetch.test.js b/ui/src/Common/Fetch.test.js index d31126888..0d777b6d6 100644 --- a/ui/src/Common/Fetch.test.js +++ b/ui/src/Common/Fetch.test.js @@ -18,7 +18,7 @@ describe("Fetch", () => { }; const methodOptions = { - FetchGet: { method: "GET" }, + FetchGet: { method: "GET", mode: "cors" }, FetchPost: { method: "POST" }, FetchDelete: { method: "DELETE" } }; @@ -59,4 +59,24 @@ describe("Fetch", () => { ); }); } + + it("FetchGet switches to no-cors after 80% failures", async () => { + fetch.mockReject(new Error("Fetch error")); + + const request = FetchGet("http://example.com", {}); + await expect(request).rejects.toThrow("Fetch error"); + + expect(fetch).toHaveBeenCalledTimes(11); + expect(fetch).toHaveBeenCalledWith("http://example.com", { + method: "GET", + credentials: "include", + mode: "no-cors", + redirect: "follow" + }); + for (let i = 0; i <= 10; i++) { + expect(fetch.mock.calls[i][1]).toMatchObject({ + mode: i < 8 ? "cors" : "no-cors" + }); + } + }); }); diff --git a/ui/src/Stores/AlertStore.js b/ui/src/Stores/AlertStore.js index 8b57f8520..29d0eed10 100644 --- a/ui/src/Stores/AlertStore.js +++ b/ui/src/Stores/AlertStore.js @@ -174,9 +174,15 @@ class AlertStore { { totalAlerts: 0, version: "unknown", - upgradeNeeded: false + upgradeNeeded: false, + reloadNeeded: false, + setReloadNeeded() { + this.reloadNeeded = true; + } + }, + { + setReloadNeeded: action }, - {}, { name: "API response info" } ); @@ -270,6 +276,13 @@ class AlertStore { return FetchGet(alertsURI, {}) .then(result => { + // we're sending requests with mode=cors so the response should also be type=cors + // after a few failures in the retry loop we will switch to no-cors + // if that request comes back as type=opaque then we might be getting + // redirected by an auth proxy + if (result.type === "opaque") { + this.info.setReloadNeeded(); + } this.status.setProcessing(); return result.json(); }) diff --git a/ui/src/Stores/AlertStore.test.js b/ui/src/Stores/AlertStore.test.js index aa7792d0c..d1e2d3522 100644 --- a/ui/src/Stores/AlertStore.test.js +++ b/ui/src/Stores/AlertStore.test.js @@ -375,7 +375,7 @@ describe("AlertStore.fetch", () => { const store = new AlertStore([]); await expect(store.fetch()).resolves.toHaveProperty("error"); - expect(global.fetch).toHaveBeenCalledTimes(6); + expect(global.fetch).toHaveBeenCalledTimes(11); expect(store.status.value).toEqual(AlertStoreStatuses.Failure); expect(store.info.version).toBe("unknown"); // there should be a trace of the error @@ -388,7 +388,7 @@ describe("AlertStore.fetch", () => { fetch.mockReject(new Error("Fetch error")); await expect(store.fetch()).resolves.toHaveProperty("error"); - expect(global.fetch).toHaveBeenCalledTimes(6); + expect(global.fetch).toHaveBeenCalledTimes(11); }); it("fetch() retry counter is reset after successful fetch", async () => { @@ -397,16 +397,34 @@ describe("AlertStore.fetch", () => { fetch.mockReject(new Error("Fetch error")); await expect(store.fetch()).resolves.toHaveProperty("error"); - expect(global.fetch).toHaveBeenCalledTimes(6); + expect(global.fetch).toHaveBeenCalledTimes(11); const response = EmptyAPIResponse(); fetch.mockResponse(JSON.stringify(response)); await expect(store.fetch()).resolves.toBeUndefined(); - expect(global.fetch).toHaveBeenCalledTimes(7); + expect(global.fetch).toHaveBeenCalledTimes(12); fetch.mockReject(new Error("Fetch error")); await expect(store.fetch()).resolves.toHaveProperty("error"); - expect(global.fetch).toHaveBeenCalledTimes(13); + expect(global.fetch).toHaveBeenCalledTimes(23); + }); + + it("fetch() reloads the page after if auth middleware is detected", async () => { + jest.spyOn(console, "trace").mockImplementation(() => {}); + + const store = new AlertStore(["label=value"]); + + jest.spyOn(global, "fetch").mockImplementation(async () => + Promise.resolve({ + type: "opaque", + body: "auth needed", + json: jest.fn(() => EmptyAPIResponse()) + }) + ); + + await expect(store.fetch()).resolves.toBeUndefined(); + + expect(store.info.reloadNeeded).toBe(true); }); it("unapplied filters are marked as applied on fetch error", async () => {