From 4ccb15018d7ca8cc6706690e859a5a004bab6213 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Sat, 21 Jan 2023 10:08:40 +0100 Subject: [PATCH] feat: rate limit error chaching Rate limit error caching to alleviate PATs. --- api/index.js | 9 +++++++-- api/pin.js | 9 +++++++-- api/top-langs.js | 9 +++++++-- api/wakatime.js | 13 +++++++------ src/common/retryer.js | 2 +- src/common/utils.js | 10 ++++++++++ tests/api.test.js | 9 +++++++-- tests/retryer.test.js | 4 ++-- 8 files changed, 48 insertions(+), 17 deletions(-) diff --git a/api/index.js b/api/index.js index b449d43b49080..251c44ed67c62 100644 --- a/api/index.js +++ b/api/index.js @@ -56,7 +56,7 @@ export default async (req, res) => { ); const cacheSeconds = clampValue( - parseInt(cache_seconds || CONSTANTS.FOUR_HOURS, 10), + parseInt(cache_seconds || CONSTANTS.CARD_CACHE_SECONDS, 10), CONSTANTS.FOUR_HOURS, CONSTANTS.ONE_DAY, ); @@ -93,7 +93,12 @@ export default async (req, res) => { }), ); } catch (err) { - res.setHeader("Cache-Control", `no-cache, no-store, must-revalidate`); // Don't cache error responses. + res.setHeader( + "Cache-Control", + `max-age=${CONSTANTS.ERROR_CACHE_SECONDS / 2}, s-maxage=${ + CONSTANTS.ERROR_CACHE_SECONDS + }, stale-while-revalidate=${CONSTANTS.ONE_DAY}`, + ); // Cache the error response less frequently. return res.send(renderError(err.message, err.secondaryMessage)); } }; diff --git a/api/pin.js b/api/pin.js index 4838b0f02fece..5e02971389d38 100644 --- a/api/pin.js +++ b/api/pin.js @@ -40,7 +40,7 @@ export default async (req, res) => { const repoData = await fetchRepo(username, repo); let cacheSeconds = clampValue( - parseInt(cache_seconds || CONSTANTS.FOUR_HOURS, 10), + parseInt(cache_seconds || CONSTANTS.CARD_CACHE_SECONDS, 10), CONSTANTS.FOUR_HOURS, CONSTANTS.ONE_DAY, ); @@ -80,7 +80,12 @@ export default async (req, res) => { }), ); } catch (err) { - res.setHeader("Cache-Control", `no-cache, no-store, must-revalidate`); // Don't cache error responses. + res.setHeader( + "Cache-Control", + `max-age=${CONSTANTS.ERROR_CACHE_SECONDS / 2}, s-maxage=${ + CONSTANTS.ERROR_CACHE_SECONDS + }, stale-while-revalidate=${CONSTANTS.ONE_DAY}`, + ); // Cache the error response less frequently. return res.send(renderError(err.message, err.secondaryMessage)); } }; diff --git a/api/top-langs.js b/api/top-langs.js index 19cccb894e33a..436172addfdc3 100644 --- a/api/top-langs.js +++ b/api/top-langs.js @@ -48,7 +48,7 @@ export default async (req, res) => { ); const cacheSeconds = clampValue( - parseInt(cache_seconds || CONSTANTS.FOUR_HOURS, 10), + parseInt(cache_seconds || CONSTANTS.CARD_CACHE_SECONDS, 10), CONSTANTS.FOUR_HOURS, CONSTANTS.ONE_DAY, ); @@ -80,7 +80,12 @@ export default async (req, res) => { }), ); } catch (err) { - res.setHeader("Cache-Control", `no-cache, no-store, must-revalidate`); // Don't cache error responses. + res.setHeader( + "Cache-Control", + `max-age=${CONSTANTS.ERROR_CACHE_SECONDS / 2}, s-maxage=${ + CONSTANTS.ERROR_CACHE_SECONDS + }, stale-while-revalidate=${CONSTANTS.ONE_DAY}`, + ); // Cache the error response less frequently. return res.send(renderError(err.message, err.secondaryMessage)); } }; diff --git a/api/wakatime.js b/api/wakatime.js index d439c5b7ac8c6..41964e371a86b 100644 --- a/api/wakatime.js +++ b/api/wakatime.js @@ -43,15 +43,11 @@ export default async (req, res) => { const stats = await fetchWakatimeStats({ username, api_domain, range }); let cacheSeconds = clampValue( - parseInt(cache_seconds || CONSTANTS.FOUR_HOURS, 10), + parseInt(cache_seconds || CONSTANTS.CARD_CACHE_SECONDS, 10), CONSTANTS.FOUR_HOURS, CONSTANTS.ONE_DAY, ); - if (!cache_seconds) { - cacheSeconds = CONSTANTS.FOUR_HOURS; - } - res.setHeader( "Cache-Control", `max-age=${ @@ -80,7 +76,12 @@ export default async (req, res) => { }), ); } catch (err) { - res.setHeader("Cache-Control", `no-cache, no-store, must-revalidate`); // Don't cache error responses. + res.setHeader( + "Cache-Control", + `max-age=${CONSTANTS.ERROR_CACHE_SECONDS / 2}, s-maxage=${ + CONSTANTS.ERROR_CACHE_SECONDS + }, stale-while-revalidate=${CONSTANTS.ONE_DAY}`, + ); // Cache the error response less frequently. return res.send(renderError(err.message, err.secondaryMessage)); } }; diff --git a/src/common/retryer.js b/src/common/retryer.js index 5351cbe8cf99a..902d13f6d976b 100644 --- a/src/common/retryer.js +++ b/src/common/retryer.js @@ -60,5 +60,5 @@ const retryer = async (fetcher, variables, retries = 0) => { } }; -export { retryer }; +export { retryer, RETRIES }; export default retryer; diff --git a/src/common/utils.js b/src/common/utils.js index 1215fc9ac8cc2..fcaa57dbfbebb 100644 --- a/src/common/utils.js +++ b/src/common/utils.js @@ -293,11 +293,21 @@ const noop = () => {}; const logger = process.env.NODE_ENV !== "test" ? console : { log: noop, error: noop }; +// Cache settings. +const CARD_CACHE_SECONDS = 14400; +const ERROR_CACHE_SECONDS = 600; + const CONSTANTS = { + ONE_MINUTE: 60, + FIVE_MINUTES: 300, + TEN_MINUTES: 600, + FIFTEEN_MINUTES: 900, THIRTY_MINUTES: 1800, TWO_HOURS: 7200, FOUR_HOURS: 14400, ONE_DAY: 86400, + CARD_CACHE_SECONDS: CARD_CACHE_SECONDS, + ERROR_CACHE_SECONDS: ERROR_CACHE_SECONDS, }; const SECONDARY_ERROR_MESSAGES = { diff --git a/tests/api.test.js b/tests/api.test.js index 0037bcdb566b2..da3a9a368c3a5 100644 --- a/tests/api.test.js +++ b/tests/api.test.js @@ -184,13 +184,18 @@ describe("Test /api/", () => { ]); }); - it("should not store cache when error", async () => { + it("should set shorter cache when error", async () => { const { req, res } = faker({}, error); await api(req, res); expect(res.setHeader.mock.calls).toEqual([ ["Content-Type", "image/svg+xml"], - ["Cache-Control", `no-cache, no-store, must-revalidate`], + [ + "Cache-Control", + `max-age=${CONSTANTS.ERROR_CACHE_SECONDS / 2}, s-maxage=${ + CONSTANTS.ERROR_CACHE_SECONDS + }, stale-while-revalidate=${CONSTANTS.ONE_DAY}`, + ], ]); }); diff --git a/tests/retryer.test.js b/tests/retryer.test.js index 1fcf6658387b9..6777386d926a3 100644 --- a/tests/retryer.test.js +++ b/tests/retryer.test.js @@ -1,6 +1,6 @@ import { jest } from "@jest/globals"; import "@testing-library/jest-dom"; -import { retryer } from "../src/common/retryer.js"; +import { retryer, RETRIES } from "../src/common/retryer.js"; import { logger } from "../src/common/utils.js"; const fetcher = jest.fn((variables, token) => { @@ -45,7 +45,7 @@ describe("Test Retryer", () => { try { res = await retryer(fetcherFail, {}); } catch (err) { - expect(fetcherFail).toBeCalledTimes(8); + expect(fetcherFail).toBeCalledTimes(RETRIES + 1); expect(err.message).toBe("Maximum retries exceeded"); } });