Skip to content
This repository was archived by the owner on Dec 10, 2021. It is now read-only.

Commit ca256cd

Browse files
schoel-biskristw
authored andcommitted
fix(connection): disable caching when on an insecure connection (#194)
When the page is served over an insecure connection, some browsers (Firefox) will disable the CacheStorage API for security reasons and will throw an error when an attempt is made to use it. Thus, do not even attempt to use CacheStorage on such connections in the first place. fix #193
1 parent 3277a2a commit ca256cd

File tree

3 files changed

+114
-67
lines changed

3 files changed

+114
-67
lines changed

packages/superset-ui-connection/src/callApi/callApi.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ export default function callApi({
2828
signal,
2929
};
3030

31-
if (method === 'GET' && CACHE_AVAILABLE) {
31+
if (
32+
method === 'GET' &&
33+
CACHE_AVAILABLE &&
34+
(self.location && self.location.protocol) === 'https:'
35+
) {
3236
return caches.open(CACHE_KEY).then(supersetCache =>
3337
supersetCache
3438
.match(url)

packages/superset-ui-connection/test/callApi/callApi.test.ts

Lines changed: 101 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -300,83 +300,118 @@ describe('callApi()', () => {
300300
});
301301
});
302302

303-
it('caches requests with ETags', () =>
304-
callApi({ url: mockCacheUrl, method: 'GET' }).then(() => {
303+
describe('caching', () => {
304+
const origLocation = self.location;
305+
306+
beforeAll(() => {
307+
Object.defineProperty(self, 'location', { value: {} });
308+
});
309+
310+
afterAll(() => {
311+
Object.defineProperty(self, 'location', { value: origLocation });
312+
});
313+
314+
beforeEach(() => {
315+
self.location.protocol = 'https:';
316+
317+
return caches.delete(constants.CACHE_KEY);
318+
});
319+
320+
it('caches requests with ETags', () =>
321+
callApi({ url: mockCacheUrl, method: 'GET' }).then(() => {
322+
const calls = fetchMock.calls(mockCacheUrl);
323+
expect(calls).toHaveLength(1);
324+
325+
return caches.open(constants.CACHE_KEY).then(supersetCache =>
326+
supersetCache.match(mockCacheUrl).then(cachedResponse => {
327+
expect(cachedResponse).toBeDefined();
328+
329+
return true;
330+
}),
331+
);
332+
}));
333+
334+
it('will not use cache when running off an insecure connection', () => {
335+
self.location.protocol = 'http:';
336+
337+
return callApi({ url: mockCacheUrl, method: 'GET' }).then(() => {
338+
const calls = fetchMock.calls(mockCacheUrl);
339+
expect(calls).toHaveLength(1);
340+
341+
return caches.open(constants.CACHE_KEY).then(supersetCache =>
342+
supersetCache.match(mockCacheUrl).then(cachedResponse => {
343+
expect(cachedResponse).toBeUndefined();
344+
345+
return true;
346+
}),
347+
);
348+
});
349+
});
350+
351+
it('works when the Cache API is disabled', async () => {
352+
Object.defineProperty(constants, 'CACHE_AVAILABLE', { value: false });
353+
354+
const firstResponse = await callApi({ url: mockCacheUrl, method: 'GET' });
305355
const calls = fetchMock.calls(mockCacheUrl);
306356
expect(calls).toHaveLength(1);
357+
const firstBody = await firstResponse.text();
358+
expect(firstBody).toEqual('BODY');
359+
360+
const secondResponse = await callApi({ url: mockCacheUrl, method: 'GET' });
361+
const fetchParams = calls[1][1];
362+
expect(calls).toHaveLength(2);
363+
// second call should not have If-None-Match header
364+
expect(fetchParams.headers).toBeUndefined();
365+
const secondBody = await secondResponse.text();
366+
expect(secondBody).toEqual('BODY');
367+
368+
Object.defineProperty(constants, 'CACHE_AVAILABLE', { value: true });
369+
});
307370

308-
return caches.open(constants.CACHE_KEY).then(supersetCache =>
309-
supersetCache.match(mockCacheUrl).then(cachedResponse => {
310-
expect(cachedResponse).toBeDefined();
371+
it('sends known ETags in the If-None-Match header', () =>
372+
// first call sets the cache
373+
callApi({ url: mockCacheUrl, method: 'GET' }).then(() => {
374+
const calls = fetchMock.calls(mockCacheUrl);
375+
expect(calls).toHaveLength(1);
376+
377+
// second call sends the Etag in the If-None-Match header
378+
return callApi({ url: mockCacheUrl, method: 'GET' }).then(() => {
379+
const fetchParams = calls[1][1];
380+
const headers = { 'If-None-Match': 'etag' };
381+
expect(calls).toHaveLength(2);
382+
expect(fetchParams.headers).toEqual(expect.objectContaining(headers));
311383

312384
return true;
313-
}),
314-
);
315-
}));
316-
317-
it('works when the Cache API is disabled', async () => {
318-
Object.defineProperty(constants, 'CACHE_AVAILABLE', { value: false });
319-
320-
const firstResponse = await callApi({ url: mockCacheUrl, method: 'GET' });
321-
const calls = fetchMock.calls(mockCacheUrl);
322-
expect(calls).toHaveLength(1);
323-
const firstBody = await firstResponse.text();
324-
expect(firstBody).toEqual('BODY');
325-
326-
const secondResponse = await callApi({ url: mockCacheUrl, method: 'GET' });
327-
const fetchParams = calls[1][1];
328-
expect(calls).toHaveLength(2);
329-
// second call should not have If-None-Match header
330-
expect(fetchParams.headers).toBeUndefined();
331-
const secondBody = await secondResponse.text();
332-
expect(secondBody).toEqual('BODY');
333-
334-
Object.defineProperty(constants, 'CACHE_AVAILABLE', { value: true });
335-
});
385+
});
386+
}));
336387

337-
it('sends known ETags in the If-None-Match header', () =>
338-
// first call sets the cache
339-
callApi({ url: mockCacheUrl, method: 'GET' }).then(() => {
388+
it('reuses cached responses on 304 status', async () => {
389+
// first call sets the cache
390+
await callApi({ url: mockCacheUrl, method: 'GET' });
340391
const calls = fetchMock.calls(mockCacheUrl);
341392
expect(calls).toHaveLength(1);
393+
// second call reuses the cached payload on a 304
394+
const mockCachedPayload = { status: 304 };
395+
fetchMock.get(mockCacheUrl, mockCachedPayload, { overwriteRoutes: true });
396+
397+
const secondResponse = await callApi({ url: mockCacheUrl, method: 'GET' });
398+
expect(calls).toHaveLength(2);
399+
const secondBody = await secondResponse.text();
400+
expect(secondBody).toEqual('BODY');
401+
});
342402

343-
// second call sends the Etag in the If-None-Match header
344-
return callApi({ url: mockCacheUrl, method: 'GET' }).then(() => {
345-
const fetchParams = calls[1][1];
346-
const headers = { 'If-None-Match': 'etag' };
347-
expect(calls).toHaveLength(2);
348-
expect(fetchParams.headers).toEqual(expect.objectContaining(headers));
403+
it('throws error when cache fails on 304', () => {
404+
// this should never happen, since a 304 is only returned if we have
405+
// the cached response and sent the If-None-Match header
406+
const mockUncachedUrl = '/mock/uncached/url';
407+
const mockCachedPayload = { status: 304 };
408+
fetchMock.get(mockUncachedUrl, mockCachedPayload);
349409

350-
return true;
410+
return callApi({ url: mockUncachedUrl, method: 'GET' }).catch(error => {
411+
const calls = fetchMock.calls(mockUncachedUrl);
412+
expect(calls).toHaveLength(1);
413+
expect(error.message).toEqual('Received 304 but no content is cached!');
351414
});
352-
}));
353-
354-
it('reuses cached responses on 304 status', async () => {
355-
// first call sets the cache
356-
await callApi({ url: mockCacheUrl, method: 'GET' });
357-
const calls = fetchMock.calls(mockCacheUrl);
358-
expect(calls).toHaveLength(1);
359-
// second call reuses the cached payload on a 304
360-
const mockCachedPayload = { status: 304 };
361-
fetchMock.get(mockCacheUrl, mockCachedPayload, { overwriteRoutes: true });
362-
363-
const secondResponse = await callApi({ url: mockCacheUrl, method: 'GET' });
364-
expect(calls).toHaveLength(2);
365-
const secondBody = await secondResponse.text();
366-
expect(secondBody).toEqual('BODY');
367-
});
368-
369-
it('throws error when cache fails on 304', () => {
370-
// this should never happen, since a 304 is only returned if we have
371-
// the cached response and sent the If-None-Match header
372-
const mockUncachedUrl = '/mock/uncached/url';
373-
const mockCachedPayload = { status: 304 };
374-
fetchMock.get(mockUncachedUrl, mockCachedPayload);
375-
376-
return callApi({ url: mockUncachedUrl, method: 'GET' }).catch(error => {
377-
const calls = fetchMock.calls(mockUncachedUrl);
378-
expect(calls).toHaveLength(1);
379-
expect(error.message).toEqual('Received 304 but no content is cached!');
380415
});
381416
});
382417

test/setupTests.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ class CacheStorage {
2525
resolve(new Cache(key));
2626
});
2727
}
28+
delete(key: string): Promise<boolean> {
29+
const wasPresent = key in caches;
30+
if (wasPresent) {
31+
caches[key] = undefined;
32+
}
33+
34+
return Promise.resolve(wasPresent);
35+
}
2836
};
2937

3038
global.caches = new CacheStorage();

0 commit comments

Comments
 (0)