Skip to content

Commit

Permalink
feat: Do not cache health checks per default (#2335)
Browse files Browse the repository at this point in the history
When using the `@HealthCheck` decorator
it will now per default set the following header:
`Cache-Control: no-cache, no-store, must-revalidate`

To disable this behavior set `@HealthCheck({ noCache: false })`

resolves #2328
  • Loading branch information
BrunnerLivio committed Sep 14, 2023
1 parent 781c00e commit 262c808
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 26 deletions.
26 changes: 26 additions & 0 deletions e2e/health-checks/health-check.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as request from 'supertest';
import { INestApplication } from '@nestjs/common';
import { DynamicHealthEndpointFn, bootstrapTestingModule } from '../helper';
import { HealthIndicatorResult } from '../../lib';

describe.only('HealthCheck', () => {
let app: INestApplication;
let setHealthEndpoint: DynamicHealthEndpointFn;

const healthyCheck = () =>
Promise.resolve<HealthIndicatorResult>({ status: 'up' } as any);

beforeEach(
() => (setHealthEndpoint = bootstrapTestingModule().setHealthEndpoint),
);

it('should set the Cache-Control header to no-cache, no-store, must-revalidate', async () => {
app = await setHealthEndpoint(({ healthCheck }) =>
healthCheck.check([healthyCheck]),
).start();

return request(app.getHttpServer())
.get('/health')
.expect('Cache-Control', 'no-cache, no-store, must-revalidate');
});
});
19 changes: 14 additions & 5 deletions e2e/helper/bootstrap-testing-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';

import {
DiskHealthIndicator,
HealthCheck,
HealthCheckResult,
HealthCheckService,
HttpHealthIndicator,
Expand All @@ -27,6 +28,7 @@ import { MongooseModule } from '@nestjs/mongoose';
import { HttpModule } from '@nestjs/axios';
import { MikroOrmHealthIndicator } from '../../lib/health-indicator/database/mikro-orm.health';
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { HealthCheckOptions } from '../../lib/health-check';

type TestingHealthFunc = (props: {
healthCheck: HealthCheckService;
Expand All @@ -41,7 +43,10 @@ type TestingHealthFunc = (props: {
prisma: PrismaHealthIndicator;
}) => Promise<HealthCheckResult>;

function createHealthController(func: TestingHealthFunc) {
function createHealthController(
func: TestingHealthFunc,
options: { healthCheckOptions?: HealthCheckOptions },
) {
@Controller()
class HealthController {
constructor(
Expand All @@ -57,6 +62,7 @@ function createHealthController(func: TestingHealthFunc) {
private readonly prisma: PrismaHealthIndicator,
) {}
@Get('health')
@HealthCheck(options.healthCheckOptions)
health() {
return func({
healthCheck: this.healthCheck,
Expand All @@ -78,7 +84,10 @@ function createHealthController(func: TestingHealthFunc) {

type PropType<TObj, TProp extends keyof TObj> = TObj[TProp];

export type DynamicHealthEndpointFn = (func: TestingHealthFunc) => {
export type DynamicHealthEndpointFn = (
func: TestingHealthFunc,
options?: { healthCheckOptions?: HealthCheckOptions },
) => {
start(
httpAdapter?: FastifyAdapter | ExpressAdapter,
): Promise<INestApplication>;
Expand All @@ -87,10 +96,10 @@ export type DynamicHealthEndpointFn = (func: TestingHealthFunc) => {
export function bootstrapTestingModule() {
const imports: PropType<ModuleMetadata, 'imports'> = [TerminusModule];

function setHealthEndpoint(func: TestingHealthFunc) {
const setHealthEndpoint: DynamicHealthEndpointFn = (func, options = {}) => {
const testingModule = Test.createTestingModule({
imports,
controllers: [createHealthController(func)],
controllers: [createHealthController(func, options)],
});

async function start(
Expand All @@ -106,7 +115,7 @@ export function bootstrapTestingModule() {
}

return { start };
}
};

function withMongoose() {
imports.push(MongooseModule.forRoot('mongodb://0.0.0.0:27017/test'));
Expand Down
85 changes: 64 additions & 21 deletions lib/health-check/health-check.decorator.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,74 @@
import { Header } from '@nestjs/common';
import { getHealthCheckSchema } from './health-check.schema';

type Swagger = typeof import('@nestjs/swagger');

// eslint-disable-next-line @typescript-eslint/no-empty-function
const noop = () => {};
/**
* @publicApi
*/
export interface HealthCheckOptions {
/**
* Whether to cache the response or not.
* - If set to `true`, the response header will be set to `Cache-Control: no-cache, no-store, must-revalidate`.
* - If set to `false`, no header will be set and can be set manually with e.g. `@Header('Cache-Control', 'max-age=604800')`.
*
* @default true
*/
noCache?: boolean;
/**
* Whether to document the endpoint with Swagger or not.
*
* @default true
*/
swaggerDocumentation?: boolean;
}

/**
* Marks the endpoint as a Health Check endpoint.
*
* - If the `@nestjs/swagger` package is installed, the endpoint will be documented.
* - Per default, the response will not be cached.
*
* @publicApi
*/
export const HealthCheck = (
{ noCache, swaggerDocumentation }: HealthCheckOptions = {
noCache: true,
swaggerDocumentation: true,
},
) => {
const decorators: MethodDecorator[] = [];

if (swaggerDocumentation) {
let swagger: Swagger | null = null;
try {
swagger = require('@nestjs/swagger');
} catch {}

if (swagger) {
decorators.push(...getSwaggerDefinitions(swagger));
}
}

if (noCache) {
const CacheControl = Header(
'Cache-Control',
'no-cache, no-store, must-revalidate',
);

decorators.push(CacheControl);
}

return (target: any, key: any, descriptor: PropertyDescriptor) => {
decorators.forEach((decorator) => {
decorator(target, key, descriptor);
});
};
};

function getSwaggerDefinitions(swagger: Swagger) {
const { ApiOkResponse, ApiServiceUnavailableResponse } = swagger;

// Possible HTTP Status
const ServiceUnavailable = ApiServiceUnavailableResponse({
description: 'The Health Check is not successful',
schema: getHealthCheckSchema('error'),
Expand All @@ -19,22 +79,5 @@ function getSwaggerDefinitions(swagger: Swagger) {
schema: getHealthCheckSchema('ok'),
});

// Combine all the SwaggerDecorators
return (target: any, key: any, descriptor: PropertyDescriptor) => {
ServiceUnavailable(target, key, descriptor);
Ok(target, key, descriptor);
};
return [ServiceUnavailable, Ok];
}

export const HealthCheck = () => {
let swagger: Swagger | null = null;
try {
// Dynamically load swagger, in case it is not installed
swagger = require('@nestjs/swagger');
} catch {}

if (swagger) {
return getSwaggerDefinitions(swagger);
}
return noop;
};

0 comments on commit 262c808

Please sign in to comment.