From a866296bc12d9f7965ee6906f899cc50d8d9818c Mon Sep 17 00:00:00 2001 From: Daniel Truong Date: Fri, 14 Aug 2026 17:33:13 +0000 Subject: [PATCH] fix: stop caching a failed /api/config app.js stamps max-age=60 on every unauthenticated GET before routing, so it lands on this route's 404 and 500 too. Once the nginx exact-match block is removed and eagle-api actually answers the path, a few seconds of Mongo trouble would be cached at rproxy and in every browser for a full minute after the database recovers. A good config stays cacheable; a failure does not. --- api/controllers/config.js | 5 +++++ test/controllers/config.test.js | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/api/controllers/config.js b/api/controllers/config.js index c571ac73..aa0ca4ae 100644 --- a/api/controllers/config.js +++ b/api/controllers/config.js @@ -44,6 +44,10 @@ exports.publicGet = async function (args, res) { if (!doc) { defaultLog.error('GET /api/config: no Config document — has the seed migration run?'); + // app.js stamps max-age=60 on every unauthenticated GET before routing. A good config + // should be cacheable; a failure must not be, or a brief outage sticks in rproxy and in + // every browser for a minute after the database comes back. + res.setHeader('Cache-Control', 'no-store'); return Actions.sendResponse(res, 404, { message: 'Configuration not found' }); } @@ -57,6 +61,7 @@ exports.publicGet = async function (args, res) { return Actions.sendResponse(res, 200, payload); } catch (err) { defaultLog.error('GET /api/config failed:', err); + res.setHeader('Cache-Control', 'no-store'); return Actions.sendResponse(res, 500, { message: 'Could not read configuration' }); } }; diff --git a/test/controllers/config.test.js b/test/controllers/config.test.js index eac513e7..6ed61a0f 100644 --- a/test/controllers/config.test.js +++ b/test/controllers/config.test.js @@ -17,6 +17,8 @@ function fakeRes() { return { statusCode: null, body: null, + headers: {}, + setHeader(name, value) { this.headers[name] = value; }, status(code) { this.statusCode = code; return this; }, json(payload) { this.body = payload; return this; } }; @@ -108,6 +110,17 @@ describe('Config Controller', () => { expect(res.statusCode).to.equal(404); }); + it('does not let the 404 be cached', async () => { + // app.js has already stamped max-age=60 on this unauthenticated GET by the time the + // controller runs — a missing config must not stick in rproxy for a minute. + stubConfigModel(null); + const res = fakeRes(); + + await configController.publicGet({}, res); + + expect(res.headers['Cache-Control']).to.equal('no-store'); + }); + it('500s when the read fails', async () => { stubConfigModel(null, new Error('connection lost')); const res = fakeRes(); @@ -117,4 +130,13 @@ describe('Config Controller', () => { expect(res.statusCode).to.equal(500); expect(res.body).to.not.have.property('stack'); }); + + it('does not let the 500 be cached', async () => { + stubConfigModel(null, new Error('connection lost')); + const res = fakeRes(); + + await configController.publicGet({}, res); + + expect(res.headers['Cache-Control']).to.equal('no-store'); + }); });