Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Do not cache health checks per default #2335

Merged
merged 1 commit into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
};