From 7c9cf783eeb663e610bf5a6abe0b9ee1cf1c6daa Mon Sep 17 00:00:00 2001 From: Justin Hartman <717118+justinhartman@users.noreply.github.com> Date: Wed, 3 Dec 2025 07:08:22 +0000 Subject: [PATCH 1/3] feat: return status codes for health checks --- api/index.spec.ts | 5 + api/index.ts | 16 +- controllers/healthController.spec.ts | 202 ++++++++++++++++++ controllers/healthController.ts | 126 +++++++++++ middleware/appLocals.spec.ts | 57 +++++ middleware/appLocals.ts | 26 +++ routes/health.ts | 24 +++ .../http/health.integration.spec.ts | 51 +++++ 8 files changed, 497 insertions(+), 10 deletions(-) create mode 100644 controllers/healthController.spec.ts create mode 100644 controllers/healthController.ts create mode 100644 middleware/appLocals.spec.ts create mode 100644 middleware/appLocals.ts create mode 100644 routes/health.ts create mode 100644 tests/integration/http/health.integration.spec.ts diff --git a/api/index.spec.ts b/api/index.spec.ts index e256c13..7e4e2ac 100644 --- a/api/index.spec.ts +++ b/api/index.spec.ts @@ -9,6 +9,7 @@ const mockApp = () => ({ const expressMock: any = jest.fn(() => mockApp()); expressMock.static = jest.fn(() => 'static'); +expressMock.Router = jest.fn(() => ({ get: jest.fn() })); jest.mock('express', () => expressMock); @@ -18,10 +19,12 @@ jest.mock('../config/db', () => connectMock); const appRouter = 'appRouter'; const authRouter = 'authRouter'; const watchlistRouter = 'watchlistRouter'; +const healthRouter = 'healthRouter'; jest.mock('../routes/app', () => appRouter); jest.mock('../routes/auth', () => authRouter); jest.mock('../routes/watchlist', () => watchlistRouter); +jest.mock('../routes/health', () => healthRouter); const baseConfig = { APP_NAME: 'name', @@ -49,6 +52,7 @@ describe('api/index', () => { expect(res.locals.APP_NAME).toBe('name'); expect(res.locals.CARD_TYPE).toBe('card-add'); expect(connectMock).toHaveBeenCalled(); + expect(app.use).toHaveBeenCalledWith('/health', healthRouter); expect(app.use).toHaveBeenCalledWith('/', appRouter); expect(app.use).toHaveBeenCalledWith('/user', authRouter); expect(app.use).toHaveBeenCalledWith('/watchlist', watchlistRouter); @@ -64,6 +68,7 @@ describe('api/index', () => { localsMiddleware({}, res, () => {}); expect(res.locals.CARD_TYPE).toBe('card'); expect(connectMock).not.toHaveBeenCalled(); + expect(app.use).toHaveBeenCalledWith('/health', healthRouter); expect(app.use).toHaveBeenCalledWith('/', appRouter); expect(app.use).not.toHaveBeenCalledWith('/user', authRouter); }); diff --git a/api/index.ts b/api/index.ts index d79f991..5f41b2f 100644 --- a/api/index.ts +++ b/api/index.ts @@ -7,7 +7,7 @@ * @license MIT */ -import express, { Request, Response, NextFunction } from 'express'; +import express from 'express'; import path from 'path'; import { inject } from '@vercel/analytics'; @@ -16,6 +16,8 @@ import connectDB from '../config/db'; import { useAuth } from '../helpers/appHelper'; import appRouter from '../routes/app'; import authRouter from '../routes/auth'; +import healthRouter from '../routes/health'; +import appLocals from '../middleware/appLocals'; import watchlistRouter from '../routes/watchlist'; /** @@ -35,15 +37,7 @@ inject({ * @param {Response} res - The response object containing the HTTP response details. * @param {NextFunction} next - The next middleware function in the chain. */ -app.use((req: Request, res: Response, next: NextFunction) => { - res.locals.APP_NAME = appConfig.APP_NAME; - res.locals.APP_SUBTITLE = appConfig.APP_SUBTITLE; - res.locals.APP_DESCRIPTION = appConfig.APP_DESCRIPTION; - res.locals.APP_URL = appConfig.APP_URL; - // We have different card types based on whether the app uses MongoDB or not. - res.locals.CARD_TYPE = useAuth ? 'card-add' : 'card'; - next(); -}); +app.use(appLocals); app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, '../views')); @@ -58,6 +52,7 @@ app.use(express.static('public')); * Load standard routes and conditionally use additional routes based on the value of useAuth boolean. * The method checks if MONGO_DB_URI is true then connects to MongoDB and uses additional middleware. */ +app.use('/health', healthRouter); app.use('/', appRouter); // Test if MONGO_DB_URI is set. if (useAuth) { @@ -75,6 +70,7 @@ if (useAuth) { * @returns {void} - No return value. */ app.listen(appConfig.API_PORT, appConfig.API_HOST, () => { + /* c8 ignore next */ console.log(`Server is running on http://${appConfig.API_HOST}:${appConfig.API_PORT}`); }); diff --git a/controllers/healthController.spec.ts b/controllers/healthController.spec.ts new file mode 100644 index 0000000..d3b0075 --- /dev/null +++ b/controllers/healthController.spec.ts @@ -0,0 +1,202 @@ +import { Request, Response } from 'express'; + +import appConfig from '../config/app'; +import httpClient from '../helpers/httpClient'; +import healthController, { checkDomainHealth } from './healthController'; + +jest.mock('../helpers/httpClient', () => ({ + __esModule: true, + default: { + get: jest.fn(), + }, +})); + +type MockedHttpClient = jest.Mocked; +const mockedHttpClient = httpClient as MockedHttpClient; + +describe('checkDomainHealth', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('returns error when domain is not configured', async () => { + const result = await checkDomainHealth('VIDSRC_DOMAIN', undefined); + expect(result).toEqual({ + name: 'VIDSRC_DOMAIN', + status: 'error', + message: 'Domain not configured', + }); + }); + + test('returns success when endpoint responds with < 400 status', async () => { + mockedHttpClient.get.mockResolvedValue({ status: 200 } as any); + const result = await checkDomainHealth('VIDSRC_DOMAIN', 'vidsrc.example'); + expect(result).toEqual({ + name: 'VIDSRC_DOMAIN', + domain: 'vidsrc.example', + status: 'success', + httpStatus: 200, + }); + expect(mockedHttpClient.get).toHaveBeenCalledWith('https://vidsrc.example', expect.any(Object)); + }); + + test('returns error when endpoint responds with >= 400 status', async () => { + mockedHttpClient.get.mockResolvedValue({ status: 404 } as any); + const result = await checkDomainHealth('VIDSRC_DOMAIN', 'vidsrc.example'); + expect(result).toEqual({ + name: 'VIDSRC_DOMAIN', + domain: 'vidsrc.example', + status: 'error', + httpStatus: 404, + message: 'Received status code 404', + }); + }); + + test('returns error when request throws', async () => { + mockedHttpClient.get.mockRejectedValue(new Error('boom')); + const result = await checkDomainHealth('VIDSRC_DOMAIN', 'vidsrc.example'); + expect(result).toEqual({ + name: 'VIDSRC_DOMAIN', + domain: 'vidsrc.example', + status: 'error', + message: 'boom', + }); + }); +}); + +describe('healthController', () => { + const createRes = () => { + const res: Partial = {}; + res.statusCode = 200; + res.status = jest.fn().mockImplementation((code: number) => { + res.statusCode = code; + return res; + }); + res.json = jest.fn().mockReturnValue(res); + return res as Response; + }; + + beforeEach(() => { + jest.clearAllMocks(); + (appConfig as any).VIDSRC_DOMAIN = 'vidsrc.example'; + (appConfig as any).MULTI_DOMAIN = 'multi.example'; + (appConfig as any).APP_URL = 'https://app.example'; + }); + + test('getEmbedDomains returns results for both domains', async () => { + mockedHttpClient.get + .mockResolvedValueOnce({ status: 200 } as any) + .mockResolvedValueOnce({ status: 503 } as any); + + const res = createRes(); + await healthController.getEmbedDomains({ query: {} } as unknown as Request, res); + + expect(res.status).toHaveBeenCalledWith(503); + expect(res.json).toHaveBeenCalledWith({ + status: 'error', + domains: [ + { + name: 'VIDSRC_DOMAIN', + domain: 'vidsrc.example', + status: 'success', + httpStatus: 200, + }, + { + name: 'MULTI_DOMAIN', + domain: 'multi.example', + status: 'error', + httpStatus: 503, + message: 'Received status code 503', + }, + ], + }); + }); + + test('getAppUrl returns single domain result', async () => { + mockedHttpClient.get.mockResolvedValue({ status: 301 } as any); + const res = createRes(); + await healthController.getAppUrl({} as Request, res); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + name: 'APP_URL', + domain: 'https://app.example', + status: 'success', + httpStatus: 301, + }); + }); + + test('getEmbedDomains handles missing MULTI_DOMAIN', async () => { + (appConfig as any).MULTI_DOMAIN = undefined; + mockedHttpClient.get.mockResolvedValue({ status: 200 } as any); + + const res = createRes(); + await healthController.getEmbedDomains({ query: {} } as unknown as Request, res); + + expect(res.status).toHaveBeenCalledWith(503); + expect(res.json).toHaveBeenCalledWith({ + status: 'error', + domains: [ + { + name: 'VIDSRC_DOMAIN', + domain: 'vidsrc.example', + status: 'success', + httpStatus: 200, + }, + { + name: 'MULTI_DOMAIN', + status: 'error', + message: 'Domain not configured', + }, + ], + }); + }); + + test('getEmbedDomains filters by target query and returns 200 when healthy', async () => { + mockedHttpClient.get.mockResolvedValue({ status: 200 } as any); + + const res = createRes(); + await healthController.getEmbedDomains({ query: { target: 'vidsrc' } } as unknown as Request, res); + + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + status: 'success', + domains: [ + { + name: 'VIDSRC_DOMAIN', + domain: 'vidsrc.example', + status: 'success', + httpStatus: 200, + }, + ], + }); + }); + + test('getEmbedDomains rejects invalid target', async () => { + const res = createRes(); + + await healthController.getEmbedDomains({ query: { target: 'unknown' } } as unknown as Request, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ + status: 'error', + message: 'Invalid target', + domains: [], + }); + }); + + test('getAppUrl returns 503 when app url is unhealthy', async () => { + mockedHttpClient.get.mockResolvedValue({ status: 500 } as any); + const res = createRes(); + + await healthController.getAppUrl({} as Request, res); + + expect(res.status).toHaveBeenCalledWith(503); + expect(res.json).toHaveBeenCalledWith({ + name: 'APP_URL', + domain: 'https://app.example', + status: 'error', + httpStatus: 500, + message: 'Received status code 500', + }); + }); +}); diff --git a/controllers/healthController.ts b/controllers/healthController.ts new file mode 100644 index 0000000..1b2d113 --- /dev/null +++ b/controllers/healthController.ts @@ -0,0 +1,126 @@ +/** + * @module controllers/healthController + * @description Controller providing health check endpoints for configured domains. + */ + +import { Request, Response } from 'express'; + +import appConfig from '../config/app'; +import httpClient from '../helpers/httpClient'; + +export type DomainHealthStatus = 'success' | 'error'; + +export interface DomainHealthResult { + name: string; + domain?: string; + status: DomainHealthStatus; + httpStatus?: number; + message?: string; +} + +const normalizeUrl = (domain: string): string => { + return /^https?:\/\//i.test(domain) ? domain : `https://${domain}`; +}; + +export const checkDomainHealth = async ( + name: string, + domain: string | undefined +): Promise => { + if (!domain) { + return { + name, + status: 'error', + message: 'Domain not configured', + }; + } + + try { + const response = await httpClient.get(normalizeUrl(domain), { + maxRedirects: 0, + /* c8 ignore next */ + validateStatus: () => true, + }); + + if (response.status >= 200 && response.status < 400) { + return { + name, + domain, + status: 'success', + httpStatus: response.status, + }; + } + + return { + name, + domain, + status: 'error', + httpStatus: response.status, + message: `Received status code ${response.status}`, + }; + } catch (error: unknown) { + let message = 'Unknown error'; + if (error instanceof Error && error.message) { + message = error.message; + } + + return { + name, + domain, + status: 'error', + message, + }; + } +}; + +const isHealthy = (results: DomainHealthResult[]): boolean => { + return results.every((result) => result.status === 'success'); +}; + +const healthController = { + /** + * Checks the configured embed domains and returns their reachability status. + * @param {Request} _req - Express request object. + * @param {Response} res - Express response object. + * @returns {Promise} JSON response containing domain health information. + */ + async getEmbedDomains(req: Request, res: Response): Promise { + const target = typeof req.query.target === 'string' ? req.query.target.toLowerCase() : undefined; + + const checks: Array> = []; + + if (!target || target === 'vidsrc') { + checks.push(checkDomainHealth('VIDSRC_DOMAIN', appConfig.VIDSRC_DOMAIN)); + } + + if (!target || target === 'multi') { + checks.push(checkDomainHealth('MULTI_DOMAIN', appConfig.MULTI_DOMAIN)); + } + + if (checks.length === 0) { + return res.status(400).json({ + status: 'error', + message: 'Invalid target', + domains: [], + }); + } + + const domains = await Promise.all(checks); + + const healthy = isHealthy(domains); + return res.status(healthy ? 200 : 503).json({ domains, status: healthy ? 'success' : 'error' }); + }, + + /** + * Checks the configured application URL and returns its reachability status. + * @param {Request} _req - Express request object. + * @param {Response} res - Express response object. + * @returns {Promise} JSON response containing APP_URL health information. + */ + async getAppUrl(_req: Request, res: Response): Promise { + const domain = await checkDomainHealth('APP_URL', appConfig.APP_URL); + const healthy = domain.status === 'success'; + return res.status(healthy ? 200 : 503).json(domain); + }, +}; + +export default healthController; diff --git a/middleware/appLocals.spec.ts b/middleware/appLocals.spec.ts new file mode 100644 index 0000000..8f6da5f --- /dev/null +++ b/middleware/appLocals.spec.ts @@ -0,0 +1,57 @@ +describe('middleware/appLocals', () => { + const configMock = { + APP_NAME: 'Test App', + APP_SUBTITLE: 'Subtitle', + APP_DESCRIPTION: 'Description', + APP_URL: 'https://example.test', + }; + + const loadMiddleware = (authEnabled: boolean) => { + let middleware: any; + jest.isolateModules(() => { + jest.doMock('../config/app', () => ({ + __esModule: true, + default: configMock, + })); + jest.doMock('../helpers/appHelper', () => ({ + useAuth: authEnabled, + })); + // eslint-disable-next-line @typescript-eslint/no-var-requires + middleware = require('./appLocals').default; + }); + return middleware as (req: any, res: any, next: () => void) => void; + }; + + afterEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + }); + + test('sets locals when authentication is disabled', () => { + const appLocals = loadMiddleware(false); + const res: any = { locals: {} }; + const next = jest.fn(); + + appLocals({}, res, next); + + expect(res.locals).toEqual({ + APP_NAME: configMock.APP_NAME, + APP_SUBTITLE: configMock.APP_SUBTITLE, + APP_DESCRIPTION: configMock.APP_DESCRIPTION, + APP_URL: configMock.APP_URL, + CARD_TYPE: 'card', + }); + expect(next).toHaveBeenCalled(); + }); + + test('sets card type to card-add when authentication is enabled', () => { + const appLocals = loadMiddleware(true); + const res: any = { locals: {} }; + const next = jest.fn(); + + appLocals({}, res, next); + + expect(res.locals.CARD_TYPE).toBe('card-add'); + expect(next).toHaveBeenCalled(); + }); +}); diff --git a/middleware/appLocals.ts b/middleware/appLocals.ts new file mode 100644 index 0000000..b02a06a --- /dev/null +++ b/middleware/appLocals.ts @@ -0,0 +1,26 @@ +/** + * @module middleware/appLocals + * @description Express middleware that populates commonly used view locals. + */ + +import { NextFunction, Request, Response } from 'express'; + +import appConfig from '../config/app'; +import { useAuth } from '../helpers/appHelper'; + +/** + * Populates Express locals with application metadata used in views. + * @param {Request} _req - Express request object (unused). + * @param {Response} res - Express response object to populate with locals. + * @param {NextFunction} next - Callback to invoke the next middleware in the chain. + */ +const appLocals = (_req: Request, res: Response, next: NextFunction): void => { + res.locals.APP_NAME = appConfig.APP_NAME; + res.locals.APP_SUBTITLE = appConfig.APP_SUBTITLE; + res.locals.APP_DESCRIPTION = appConfig.APP_DESCRIPTION; + res.locals.APP_URL = appConfig.APP_URL; + res.locals.CARD_TYPE = useAuth ? 'card-add' : 'card'; + next(); +}; + +export default appLocals; diff --git a/routes/health.ts b/routes/health.ts new file mode 100644 index 0000000..bcc050f --- /dev/null +++ b/routes/health.ts @@ -0,0 +1,24 @@ +/** + * @module routes/health + * @description Express router providing health check endpoints. + */ + +import { Router } from 'express'; + +import healthController from '../controllers/healthController'; + +const router = Router(); + +/** + * @route GET /health/domains + * @description Returns the health status of configured embed domains. + */ +router.get('/domains', healthController.getEmbedDomains); + +/** + * @route GET /health/app + * @description Returns the health status of the application domain. + */ +router.get('/app', healthController.getAppUrl); + +export default router; diff --git a/tests/integration/http/health.integration.spec.ts b/tests/integration/http/health.integration.spec.ts new file mode 100644 index 0000000..d694d42 --- /dev/null +++ b/tests/integration/http/health.integration.spec.ts @@ -0,0 +1,51 @@ +jest.mock('../../../controllers/healthController', () => ({ + __esModule: true, + default: { + getEmbedDomains: (_req: any, res: any) => res.json({ ok: true }), + getAppUrl: (_req: any, res: any) => res.json({ status: 'ok' }), + }, +})); + +import healthRouter from '../../../routes/health'; + +describe('health routes', () => { + const findRoute = (method: 'get', path: string) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const layer = (healthRouter as any).stack.find( + (entry: any) => entry.route && entry.route.path === path && entry.route.methods[method] + ); + return layer ? layer.route.stack[0].handle : undefined; + }; + + const createMockRes = () => { + const res: any = { statusCode: 200, body: undefined }; + res.json = (payload: unknown) => { + res.body = payload; + return res; + }; + return res; + }; + + test('GET /domains should exist and invoke controller', async () => { + const handler = findRoute('get', '/domains'); + expect(handler).toBeDefined(); + + const res = createMockRes(); + const payload = { ok: true }; + const next = jest.fn(); + await handler!({}, res, next); + expect(res.body).toEqual(payload); + expect(next).not.toHaveBeenCalled(); + }); + + test('GET /app should exist and invoke controller', async () => { + const handler = findRoute('get', '/app'); + expect(handler).toBeDefined(); + + const res = createMockRes(); + const next = jest.fn(); + await handler!({}, res, next); + expect(res.body).toEqual({ status: 'ok' }); + expect(next).not.toHaveBeenCalled(); + }); +}); From 8aa1b350e07a234996bc2e8a7ba670ef17b77f69 Mon Sep 17 00:00:00 2001 From: Justin Hartman <717118+justinhartman@users.noreply.github.com> Date: Wed, 3 Dec 2025 07:37:21 +0000 Subject: [PATCH 2/3] refactor: Handle MULTI_DOMAIN as an optional config when checking health of domains --- controllers/healthController.spec.ts | 23 ++++++++++++++++------- controllers/healthController.ts | 16 ++++++++++++++-- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/controllers/healthController.spec.ts b/controllers/healthController.spec.ts index d3b0075..5faca8f 100644 --- a/controllers/healthController.spec.ts +++ b/controllers/healthController.spec.ts @@ -132,9 +132,9 @@ describe('healthController', () => { const res = createRes(); await healthController.getEmbedDomains({ query: {} } as unknown as Request, res); - expect(res.status).toHaveBeenCalledWith(503); + expect(res.status).toHaveBeenCalledWith(200); expect(res.json).toHaveBeenCalledWith({ - status: 'error', + status: 'success', domains: [ { name: 'VIDSRC_DOMAIN', @@ -142,15 +142,24 @@ describe('healthController', () => { status: 'success', httpStatus: 200, }, - { - name: 'MULTI_DOMAIN', - status: 'error', - message: 'Domain not configured', - }, ], }); }); + test('getEmbedDomains rejects multi target when MULTI_DOMAIN is not configured', async () => { + (appConfig as any).MULTI_DOMAIN = undefined; + const res = createRes(); + + await healthController.getEmbedDomains({ query: { target: 'multi' } } as unknown as Request, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ + status: 'error', + message: 'Target not configured', + domains: [], + }); + }); + test('getEmbedDomains filters by target query and returns 200 when healthy', async () => { mockedHttpClient.get.mockResolvedValue({ status: 200 } as any); diff --git a/controllers/healthController.ts b/controllers/healthController.ts index 1b2d113..335ab49 100644 --- a/controllers/healthController.ts +++ b/controllers/healthController.ts @@ -82,10 +82,19 @@ const healthController = { * @param {Request} _req - Express request object. * @param {Response} res - Express response object. * @returns {Promise} JSON response containing domain health information. - */ + */ async getEmbedDomains(req: Request, res: Response): Promise { const target = typeof req.query.target === 'string' ? req.query.target.toLowerCase() : undefined; + // Return 400 if there is no `MULTI_DOMAIN` configured + if (target === 'multi' && !appConfig.MULTI_DOMAIN) { + return res.status(400).json({ + status: 'error', + message: 'Target not configured', + domains: [], + }); + } + const checks: Array> = []; if (!target || target === 'vidsrc') { @@ -93,7 +102,10 @@ const healthController = { } if (!target || target === 'multi') { - checks.push(checkDomainHealth('MULTI_DOMAIN', appConfig.MULTI_DOMAIN)); + // This makes sure we make this optional as there will be scenarios where MULTI_DOMAIN isn't configured + if (appConfig.MULTI_DOMAIN) { + checks.push(checkDomainHealth('MULTI_DOMAIN', appConfig.MULTI_DOMAIN)); + } } if (checks.length === 0) { From 45006a8d067c517b06129e45c8b503b9a8e04f8f Mon Sep 17 00:00:00 2001 From: Justin Hartman <717118+justinhartman@users.noreply.github.com> Date: Wed, 3 Dec 2025 08:01:48 +0000 Subject: [PATCH 3/3] refactor: Add cache buster to prevent API caching on health checks --- controllers/healthController.spec.ts | 14 +++++++++++++- controllers/healthController.ts | 18 ++++++++++++++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/controllers/healthController.spec.ts b/controllers/healthController.spec.ts index 5faca8f..6726a8e 100644 --- a/controllers/healthController.spec.ts +++ b/controllers/healthController.spec.ts @@ -72,6 +72,7 @@ describe('healthController', () => { res.statusCode = code; return res; }); + res.set = jest.fn().mockReturnValue(res); res.json = jest.fn().mockReturnValue(res); return res as Response; }; @@ -90,7 +91,12 @@ describe('healthController', () => { const res = createRes(); await healthController.getEmbedDomains({ query: {} } as unknown as Request, res); - + expect(res.set).toHaveBeenCalledWith({ + 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate', + Pragma: 'no-cache', + Expires: '0', + 'Surrogate-Control': 'no-store', + }); expect(res.status).toHaveBeenCalledWith(503); expect(res.json).toHaveBeenCalledWith({ status: 'error', @@ -116,6 +122,12 @@ describe('healthController', () => { mockedHttpClient.get.mockResolvedValue({ status: 301 } as any); const res = createRes(); await healthController.getAppUrl({} as Request, res); + expect(res.set).toHaveBeenCalledWith({ + 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate', + Pragma: 'no-cache', + Expires: '0', + 'Surrogate-Control': 'no-store', + }); expect(res.status).toHaveBeenCalledWith(200); expect(res.json).toHaveBeenCalledWith({ name: 'APP_URL', diff --git a/controllers/healthController.ts b/controllers/healthController.ts index 335ab49..a744019 100644 --- a/controllers/healthController.ts +++ b/controllers/healthController.ts @@ -18,7 +18,7 @@ export interface DomainHealthResult { message?: string; } -const normalizeUrl = (domain: string): string => { +const normaliseUrl = (domain: string): string => { return /^https?:\/\//i.test(domain) ? domain : `https://${domain}`; }; @@ -35,7 +35,7 @@ export const checkDomainHealth = async ( } try { - const response = await httpClient.get(normalizeUrl(domain), { + const response = await httpClient.get(normaliseUrl(domain), { maxRedirects: 0, /* c8 ignore next */ validateStatus: () => true, @@ -76,14 +76,24 @@ const isHealthy = (results: DomainHealthResult[]): boolean => { return results.every((result) => result.status === 'success'); }; +const setNoCacheHeaders = (res: Response): void => { + res.set({ + 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate', + Pragma: 'no-cache', + Expires: '0', + 'Surrogate-Control': 'no-store', + }); +}; + const healthController = { /** * Checks the configured embed domains and returns their reachability status. * @param {Request} _req - Express request object. * @param {Response} res - Express response object. * @returns {Promise} JSON response containing domain health information. - */ + */ async getEmbedDomains(req: Request, res: Response): Promise { + setNoCacheHeaders(res); const target = typeof req.query.target === 'string' ? req.query.target.toLowerCase() : undefined; // Return 400 if there is no `MULTI_DOMAIN` configured @@ -117,7 +127,6 @@ const healthController = { } const domains = await Promise.all(checks); - const healthy = isHealthy(domains); return res.status(healthy ? 200 : 503).json({ domains, status: healthy ? 'success' : 'error' }); }, @@ -129,6 +138,7 @@ const healthController = { * @returns {Promise} JSON response containing APP_URL health information. */ async getAppUrl(_req: Request, res: Response): Promise { + setNoCacheHeaders(res); const domain = await checkDomainHealth('APP_URL', appConfig.APP_URL); const healthy = domain.status === 'success'; return res.status(healthy ? 200 : 503).json(domain);