feat(ui): try to detect if API requests are intercepted by an auth proxy

This commit is contained in:
Łukasz Mierzwa
2019-12-21 18:33:10 +00:00
parent dfce9332e7
commit 54550bacf9
4 changed files with 76 additions and 13 deletions

View File

@@ -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
);

View File

@@ -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"
});
}
});
});

View File

@@ -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();
})

View File

@@ -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 () => {