Skip to content

Commit b1c04c0

Browse files
Merge pull request #61 from justinhartman/feat/create-health-check-api-for-domains
Return status codes for health checks
2 parents 5a69054 + 45006a8 commit b1c04c0

File tree

2 files changed

+142
-17
lines changed

2 files changed

+142
-17
lines changed

controllers/healthController.spec.ts

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe('checkDomainHealth', () => {
2020
});
2121

2222
test('returns error when domain is not configured', async () => {
23-
const result = await checkDomainHealth('VIDSRC_DOMAIN');
23+
const result = await checkDomainHealth('VIDSRC_DOMAIN', undefined);
2424
expect(result).toEqual({
2525
name: 'VIDSRC_DOMAIN',
2626
status: 'error',
@@ -67,6 +67,12 @@ describe('checkDomainHealth', () => {
6767
describe('healthController', () => {
6868
const createRes = () => {
6969
const res: Partial<Response> = {};
70+
res.statusCode = 200;
71+
res.status = jest.fn().mockImplementation((code: number) => {
72+
res.statusCode = code;
73+
return res;
74+
});
75+
res.set = jest.fn().mockReturnValue(res);
7076
res.json = jest.fn().mockReturnValue(res);
7177
return res as Response;
7278
};
@@ -84,9 +90,16 @@ describe('healthController', () => {
8490
.mockResolvedValueOnce({ status: 503 } as any);
8591

8692
const res = createRes();
87-
await healthController.getEmbedDomains({} as Request, res);
88-
93+
await healthController.getEmbedDomains({ query: {} } as unknown as Request, res);
94+
expect(res.set).toHaveBeenCalledWith({
95+
'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
96+
Pragma: 'no-cache',
97+
Expires: '0',
98+
'Surrogate-Control': 'no-store',
99+
});
100+
expect(res.status).toHaveBeenCalledWith(503);
89101
expect(res.json).toHaveBeenCalledWith({
102+
status: 'error',
90103
domains: [
91104
{
92105
name: 'VIDSRC_DOMAIN',
@@ -109,6 +122,13 @@ describe('healthController', () => {
109122
mockedHttpClient.get.mockResolvedValue({ status: 301 } as any);
110123
const res = createRes();
111124
await healthController.getAppUrl({} as Request, res);
125+
expect(res.set).toHaveBeenCalledWith({
126+
'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
127+
Pragma: 'no-cache',
128+
Expires: '0',
129+
'Surrogate-Control': 'no-store',
130+
});
131+
expect(res.status).toHaveBeenCalledWith(200);
112132
expect(res.json).toHaveBeenCalledWith({
113133
name: 'APP_URL',
114134
domain: 'https://app.example',
@@ -122,22 +142,82 @@ describe('healthController', () => {
122142
mockedHttpClient.get.mockResolvedValue({ status: 200 } as any);
123143

124144
const res = createRes();
125-
await healthController.getEmbedDomains({} as Request, res);
145+
await healthController.getEmbedDomains({ query: {} } as unknown as Request, res);
126146

147+
expect(res.status).toHaveBeenCalledWith(200);
127148
expect(res.json).toHaveBeenCalledWith({
149+
status: 'success',
128150
domains: [
129151
{
130152
name: 'VIDSRC_DOMAIN',
131153
domain: 'vidsrc.example',
132154
status: 'success',
133155
httpStatus: 200,
134156
},
157+
],
158+
});
159+
});
160+
161+
test('getEmbedDomains rejects multi target when MULTI_DOMAIN is not configured', async () => {
162+
(appConfig as any).MULTI_DOMAIN = undefined;
163+
const res = createRes();
164+
165+
await healthController.getEmbedDomains({ query: { target: 'multi' } } as unknown as Request, res);
166+
167+
expect(res.status).toHaveBeenCalledWith(400);
168+
expect(res.json).toHaveBeenCalledWith({
169+
status: 'error',
170+
message: 'Target not configured',
171+
domains: [],
172+
});
173+
});
174+
175+
test('getEmbedDomains filters by target query and returns 200 when healthy', async () => {
176+
mockedHttpClient.get.mockResolvedValue({ status: 200 } as any);
177+
178+
const res = createRes();
179+
await healthController.getEmbedDomains({ query: { target: 'vidsrc' } } as unknown as Request, res);
180+
181+
expect(res.status).toHaveBeenCalledWith(200);
182+
expect(res.json).toHaveBeenCalledWith({
183+
status: 'success',
184+
domains: [
135185
{
136-
name: 'MULTI_DOMAIN',
137-
status: 'error',
138-
message: 'Domain not configured',
186+
name: 'VIDSRC_DOMAIN',
187+
domain: 'vidsrc.example',
188+
status: 'success',
189+
httpStatus: 200,
139190
},
140191
],
141192
});
142193
});
194+
195+
test('getEmbedDomains rejects invalid target', async () => {
196+
const res = createRes();
197+
198+
await healthController.getEmbedDomains({ query: { target: 'unknown' } } as unknown as Request, res);
199+
200+
expect(res.status).toHaveBeenCalledWith(400);
201+
expect(res.json).toHaveBeenCalledWith({
202+
status: 'error',
203+
message: 'Invalid target',
204+
domains: [],
205+
});
206+
});
207+
208+
test('getAppUrl returns 503 when app url is unhealthy', async () => {
209+
mockedHttpClient.get.mockResolvedValue({ status: 500 } as any);
210+
const res = createRes();
211+
212+
await healthController.getAppUrl({} as Request, res);
213+
214+
expect(res.status).toHaveBeenCalledWith(503);
215+
expect(res.json).toHaveBeenCalledWith({
216+
name: 'APP_URL',
217+
domain: 'https://app.example',
218+
status: 'error',
219+
httpStatus: 500,
220+
message: 'Received status code 500',
221+
});
222+
});
143223
});

controllers/healthController.ts

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ export interface DomainHealthResult {
1818
message?: string;
1919
}
2020

21-
const normalizeUrl = (domain: string): string => {
21+
const normaliseUrl = (domain: string): string => {
2222
return /^https?:\/\//i.test(domain) ? domain : `https://${domain}`;
2323
};
2424

2525
export const checkDomainHealth = async (
2626
name: string,
27-
domain?: string
27+
domain: string | undefined
2828
): Promise<DomainHealthResult> => {
2929
if (!domain) {
3030
return {
@@ -35,7 +35,7 @@ export const checkDomainHealth = async (
3535
}
3636

3737
try {
38-
const response = await httpClient.get(normalizeUrl(domain), {
38+
const response = await httpClient.get(normaliseUrl(domain), {
3939
maxRedirects: 0,
4040
/* c8 ignore next */
4141
validateStatus: () => true,
@@ -72,20 +72,63 @@ export const checkDomainHealth = async (
7272
}
7373
};
7474

75+
const isHealthy = (results: DomainHealthResult[]): boolean => {
76+
return results.every((result) => result.status === 'success');
77+
};
78+
79+
const setNoCacheHeaders = (res: Response): void => {
80+
res.set({
81+
'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
82+
Pragma: 'no-cache',
83+
Expires: '0',
84+
'Surrogate-Control': 'no-store',
85+
});
86+
};
87+
7588
const healthController = {
7689
/**
7790
* Checks the configured embed domains and returns their reachability status.
7891
* @param {Request} _req - Express request object.
7992
* @param {Response} res - Express response object.
8093
* @returns {Promise<Response>} JSON response containing domain health information.
8194
*/
82-
async getEmbedDomains(_req: Request, res: Response): Promise<Response> {
83-
const domains = await Promise.all([
84-
checkDomainHealth('VIDSRC_DOMAIN', appConfig.VIDSRC_DOMAIN),
85-
checkDomainHealth('MULTI_DOMAIN', appConfig.MULTI_DOMAIN),
86-
]);
95+
async getEmbedDomains(req: Request, res: Response): Promise<Response> {
96+
setNoCacheHeaders(res);
97+
const target = typeof req.query.target === 'string' ? req.query.target.toLowerCase() : undefined;
98+
99+
// Return 400 if there is no `MULTI_DOMAIN` configured
100+
if (target === 'multi' && !appConfig.MULTI_DOMAIN) {
101+
return res.status(400).json({
102+
status: 'error',
103+
message: 'Target not configured',
104+
domains: [],
105+
});
106+
}
107+
108+
const checks: Array<Promise<DomainHealthResult>> = [];
109+
110+
if (!target || target === 'vidsrc') {
111+
checks.push(checkDomainHealth('VIDSRC_DOMAIN', appConfig.VIDSRC_DOMAIN));
112+
}
113+
114+
if (!target || target === 'multi') {
115+
// This makes sure we make this optional as there will be scenarios where MULTI_DOMAIN isn't configured
116+
if (appConfig.MULTI_DOMAIN) {
117+
checks.push(checkDomainHealth('MULTI_DOMAIN', appConfig.MULTI_DOMAIN));
118+
}
119+
}
120+
121+
if (checks.length === 0) {
122+
return res.status(400).json({
123+
status: 'error',
124+
message: 'Invalid target',
125+
domains: [],
126+
});
127+
}
87128

88-
return res.json({ domains });
129+
const domains = await Promise.all(checks);
130+
const healthy = isHealthy(domains);
131+
return res.status(healthy ? 200 : 503).json({ domains, status: healthy ? 'success' : 'error' });
89132
},
90133

91134
/**
@@ -95,8 +138,10 @@ const healthController = {
95138
* @returns {Promise<Response>} JSON response containing APP_URL health information.
96139
*/
97140
async getAppUrl(_req: Request, res: Response): Promise<Response> {
141+
setNoCacheHeaders(res);
98142
const domain = await checkDomainHealth('APP_URL', appConfig.APP_URL);
99-
return res.json(domain);
143+
const healthy = domain.status === 'success';
144+
return res.status(healthy ? 200 : 503).json(domain);
100145
},
101146
};
102147

0 commit comments

Comments
 (0)