🐈

NestJS はデフォルトで Cache-Control ヘッダーを設定しない(ので一括で設定する)

2023/12/26に公開

ライフイズテック株式会社 サービス開発部の山口 (@no_clock) です。塾プロダクトグループでソフトウェアエンジニアをしています。今回は、 Node.js の Web アプリケーションフレームワーク NestJS の小ネタです。

結論

  • Interceptor を作成・指定すると、一括でレスポンスヘッダーを設定できる。

NestJS はデフォルトで Cache-Control ヘッダーを設定しない

NestJS は Cache-Control ヘッダーを設定しません (10.3.0 時点)。 Ruby on Rails ではデフォルトで設定されていたため、その感覚で触っていて気づかずハマりました。

NestJS のレスポンスヘッダー
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 12
ETag: W/"c-Lve95gjOVATpfV8EL5X4nxwjKHE"
Date: Tue, 19 Dec 2023 14:19:15 GMT
Connection: keep-alive
Keep-Alive: timeout=5

Interceptor で一括設定する

NestJS のドキュメントには、 @Header() デコレーターを使ってレスポンスヘッダーを設定する例があります。ただ、個々で設定するのはたいへん手間です。

@Post()
@Header('Cache-Control', 'none')
create() {
  return 'This action adds a new cat';
}

Controllers | NestJS - A progressive Node.js framework

そこで、 Interceptor を使います。 Interceptor は、リクエストの処理前後に共通の処理を差し込むことができます[1]

コード

NoCacheInterceptor クラスを作成し、 useGlobalInterceptors() 関数でアプリケーション全体に適用します。

src/no-cache-interceptor.ts
import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';

export class NoCacheInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    context
      .switchToHttp()
      .getResponse()
      .header('Cache-Control', 'private, no-store, no-cache, must-revalidate');
    return next.handle();
  }
}
src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { NoCacheInterceptor } from 'utils/no-cache-interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalInterceptors(new NoCacheInterceptor());
  await app.listen(3000);
}
bootstrap();

設定後のレスポンスヘッダー

Cache-Control ヘッダーが設定されました。

X-Powered-By: Express
Cache-Control: private, no-store, no-cache, must-revalidate
Content-Type: text/html; charset=utf-8
Content-Length: 12
ETag: W/"c-Lve95gjOVATpfV8EL5X4nxwjKHE"
Date: Wed, 20 Dec 2023 02:38:51 GMT
Connection: keep-alive
Keep-Alive: timeout=5

おまけ

Cache-Control ヘッダーを個別に変更できるようにする

NoCacheInterceptor を変更すると、 @Header() デコレーターを使った個別設定も可能になります。

src/no-cache-interceptor.ts (抜粋)
import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Response } from 'express';

export class NoCacheInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const response: Response = context.switchToHttp().getResponse();
    if (!response.getHeader('Cache-Control')) {
      response.header(
        'Cache-Control',
        'private, no-store, no-cache, must-revalidate',
      );
    }
    return next.handle();
  }
}

Ruby on Rails と Spring Boot (Spring Framework) のレスポンスヘッダー

記事作成にあたって確認した他フレームワークのレスポンスヘッダーをおまけで置いておきます。

Ruby on Rails 7.1.2

x-frame-options: SAMEORIGIN
x-xss-protection: 0
x-content-type-options: nosniff
x-permitted-cross-domain-policies: none
referrer-policy: strict-origin-when-cross-origin
content-type: text/html; charset=utf-8
vary: Accept
etag: W/"e94d902b215196bfa9fba8479de699f9"
cache-control: max-age=0, private, must-revalidate
Content-Length: 2256
(略)

Spring Boot 3.2.0 (+ Spring Web / Java 21)

Content-Type: text/html;charset=UTF-8
Content-Length: 12
Date: Tue, 19 Dec 2023 14:52:55 GMT
Keep-Alive: timeout=60
Connection: keep-alive

※ Spring Security を追加すると Cache-Control ヘッダーなどが付きます(Security HTTP Response Headers :: Spring Security)。ただし、デフォルトで全エンドポイントに認証が要求されるようにもなります(Hello Spring Security :: Spring Security)。

ちょっと宣伝

ライフイズテック サービス開発部では、気軽にご参加いただけるカジュアルなイベントを実施しています。開催予定のイベントは、 connpass のライフイズテックグループからご確認ください!

参考

脚注
  1. アスペクト指向プログラミング (AOP) に近い思想で、実際ドキュメントにも AOP の影響を受けているとの記載があります ↩︎

Discussion