From e8e5cf86fba90f36735372006fad12f8ea4c9b69 Mon Sep 17 00:00:00 2001 From: Ophelia Goldstein <159258143+opheliagoldstein@users.noreply.github.com> Date: Wed, 8 Oct 2025 01:19:54 +0300 Subject: [PATCH] feat: disable cache with proper headers when CACHE_SECONDS env is zero (#4539) * feat: disable cache with proper headers when CACHE_SECONDS env is zero * Update src/common/cache.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Alexandr Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/common/cache.js | 37 ++++++++++++++++++++++++++++++++++--- tests/api.test.js | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/src/common/cache.js b/src/common/cache.js index c39a4b5d..a4e68113 100644 --- a/src/common/cache.js +++ b/src/common/cache.js @@ -15,13 +15,31 @@ import { clampValue, CONSTANTS } from "./utils.js"; const resolveCacheSeconds = ({ requested, def, min, max }) => { let cacheSeconds = clampValue(isNaN(requested) ? def : requested, min, max); - cacheSeconds = process.env.CACHE_SECONDS - ? parseInt(process.env.CACHE_SECONDS, 10) || cacheSeconds - : cacheSeconds; + if (process.env.CACHE_SECONDS) { + const envCacheSeconds = parseInt(process.env.CACHE_SECONDS, 10); + if (!isNaN(envCacheSeconds)) { + cacheSeconds = envCacheSeconds; + } + } return cacheSeconds; }; +/** + * Disables caching by setting appropriate headers on the response object. + * + * @param {Object} res The response object. + */ +const disableCaching = (res) => { + // Disable caching for browsers, shared caches/CDNs, and GitHub Camo. + res.setHeader( + "Cache-Control", + "no-cache, no-store, must-revalidate, max-age=0, s-maxage=0", + ); + res.setHeader("Pragma", "no-cache"); + res.setHeader("Expires", "0"); +}; + /** * Sets the Cache-Control headers on the response object. * @@ -29,6 +47,11 @@ const resolveCacheSeconds = ({ requested, def, min, max }) => { * @param {number} cacheSeconds The cache seconds to set in the headers. */ const setCacheHeaders = (res, cacheSeconds) => { + if (cacheSeconds < 1) { + disableCaching(res); + return; + } + res.setHeader( "Cache-Control", `max-age=${cacheSeconds}, ` + @@ -43,6 +66,14 @@ const setCacheHeaders = (res, cacheSeconds) => { * @param {Object} res The response object. */ const setErrorCacheHeaders = (res) => { + const envCacheSeconds = process.env.CACHE_SECONDS + ? parseInt(process.env.CACHE_SECONDS, 10) + : NaN; + if (!isNaN(envCacheSeconds) && envCacheSeconds < 1) { + disableCaching(res); + return; + } + // Use lower cache period for errors. res.setHeader( "Cache-Control", diff --git a/tests/api.test.js b/tests/api.test.js index 2b4f83c9..bd255455 100644 --- a/tests/api.test.js +++ b/tests/api.test.js @@ -1,6 +1,6 @@ // @ts-check -import { jest } from "@jest/globals"; +import { beforeEach, jest } from "@jest/globals"; import axios from "axios"; import MockAdapter from "axios-mock-adapter"; import api from "../api/index.js"; @@ -99,6 +99,10 @@ const faker = (query, data) => { return { req, res }; }; +beforeEach(() => { + process.env.CACHE_SECONDS = undefined; +}); + afterEach(() => { mock.reset(); }); @@ -224,6 +228,38 @@ describe("Test /api/", () => { ]); }); + it("should properly set cache using CACHE_SECONDS env variable", async () => { + process.env.CACHE_SECONDS = "10000"; + + const { req, res } = faker({}, data_stats); + await api(req, res); + + expect(res.setHeader.mock.calls).toEqual([ + ["Content-Type", "image/svg+xml"], + [ + "Cache-Control", + `max-age=10000, s-maxage=10000, stale-while-revalidate=${CONSTANTS.ONE_DAY}`, + ], + ]); + }); + + it("should disable cache when CACHE_SECONDS is set to 0", async () => { + process.env.CACHE_SECONDS = "0"; + + const { req, res } = faker({}, data_stats); + await api(req, res); + + expect(res.setHeader.mock.calls).toEqual([ + ["Content-Type", "image/svg+xml"], + [ + "Cache-Control", + "no-cache, no-store, must-revalidate, max-age=0, s-maxage=0", + ], + ["Pragma", "no-cache"], + ["Expires", "0"], + ]); + }); + it("should set proper cache with clamped values", async () => { { let { req, res } = faker({ cache_seconds: 200_000 }, data_stats);