ミドルウェア

ミドルウェア

ミドルウェアは、HTTPリクエストがルートハンドラに到達する前に実行される一連の関数です。チェーン内の各関数はリクエストを終了させるか、次のミドルウェアに転送できます。

典型的なAdonisJSアプリケーションでは、リクエストボディの解析ユーザーセッションの管理リクエストの認証静的アセットの提供などのためにミドルウェアが使用されます。

また、カスタムミドルウェアを作成してHTTPリクエスト中に追加のタスクを実行することもできます。

ミドルウェアスタック

ミドルウェアパイプラインの実行をより制御するために、AdonisJSはミドルウェアスタックを次の3つのグループに分割しています。

サーバーミドルウェアスタック

サーバーミドルウェアは、現在のリクエストのURLに対してルートが定義されていなくても、すべてのHTTPリクエストで実行されます。

これは、フレームワークのルーティングシステムに依存しないアプリケーションの追加機能を追加するために使用できます。たとえば、静的アセットミドルウェアはサーバーミドルウェアとして登録されています。

サーバーミドルウェアは、start/kernel.tsファイル内のserve.useメソッドを使用して登録できます。

import server from '@adonisjs/core/services/server'
server.use([
() => import('@adonisjs/static/static_middleware')
])

ルーターミドルウェアスタック

ルーターミドルウェアは、一致するルートを持つすべてのHTTPリクエストで実行されるグローバルミドルウェアとしても知られています。

Bodyparser、auth、sessionミドルウェアは、ルーターミドルウェアスタックに登録されています。

ルーターミドルウェアは、start/kernel.tsファイル内のrouter.useメソッドを使用して登録できます。

import router from '@adonisjs/core/services/router'
router.use([
() => import('@adonisjs/core/bodyparser_middleware')
])

名前付きミドルウェアコレクション

名前付きミドルウェアは、ルートまたはグループに明示的に割り当てられない限り実行されないミドルウェアのコレクションです。

ルートファイル内でミドルウェアをインラインコールバックとして定義する代わりに、専用のミドルウェアクラスを作成し、名前付きミドルウェアコレクションに格納し、それをルートに割り当てることをオススメします。

名前付きミドルウェアは、start/kernel.tsファイル内のrouter.namedメソッドを使用して定義できます。名前付きコレクションを使用するためには、名前付きコレクションをエクスポートする必要があります ルートファイル内で使用するため

import router from '@adonisjs/core/services/router'
router.named({
auth: () => import('#middleware/auth_middleware')
})

ミドルウェアの作成

ミドルウェアは./app/middlewareディレクトリ内に格納され、make:middlewareエースコマンドを実行して新しいミドルウェアファイルを作成できます。

参照: ミドルウェア作成コマンド

node ace make:middleware user_location

上記のコマンドは、ミドルウェアディレクトリの下にuser_location_middleware.tsファイルを作成します。

ミドルウェアはhandleメソッドを持つクラスとして表されます。実行中、AdonisJSは自動的にこのメソッドを呼び出し、最初の引数としてHttpContextを渡します。

app/middleware/user_location_middleware.ts
import { HttpContext } from '@adonisjs/core/http'
import { NextFn } from '@adonisjs/core/types/http'
export default class UserLocationMiddleware {
async handle(ctx: HttpContext, next: NextFn) {
}
}

ミドルウェア内のhandleメソッドでは、リクエストの継続、レスポンスの送信、またはリクエストの中断のために例外を発生させるかを決定する必要があります。

リクエストの中断

ミドルウェアが例外を発生させると、次のミドルウェアとルートハンドラは実行されず、例外はグローバルな例外ハンドラに渡されます。

import { Exception } from '@adonisjs/core/exceptions'
import { NextFn } from '@adonisjs/core/types/http'
export default class UserLocationMiddleware {
async handle(ctx: HttpContext, next: NextFn) {
throw new Exception('リクエストを中断します')
}
}

リクエストの継続

リクエストを継続するには、nextメソッドを呼び出す必要があります。そうしないと、ミドルウェアスタック内の残りのアクションは実行されません。

export default class UserLocationMiddleware {
async handle(ctx: HttpContext, next: NextFn) {
// `next`関数を呼び出して継続します
await next()
}
}

レスポンスを送信し、nextメソッドを呼び出さない

最後に、レスポンスを送信してリクエストを終了することもできます。この場合、nextメソッドを呼び出さないでください。

export default class UserLocationMiddleware {
async handle(ctx: HttpContext, next: NextFn) {
// レスポンスを送信し、`next`を呼び出さない
ctx.response.send('リクエストを終了します')
}
}

ミドルウェアのルートとルートグループへの割り当て

名前付きミドルウェアコレクションはデフォルトでは使用されず、明示的にルートまたはルートグループに割り当てる必要があります。

次の例では、middlewareコレクションをインポートし、userLocationミドルウェアをルートに割り当てています。

import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'
router
.get('posts', () => {})
.use(middleware.userLocation())

複数のミドルウェアは、配列として適用するか、useメソッドを複数回呼び出すことで適用できます。

router
.get('posts', () => {})
.use([
middleware.userLocation(),
middleware.auth()
])

同様に、ルートグループにもミドルウェアを割り当てることができます。グループミドルウェアは、グループ内のすべてのルートに自動的に適用されます。

import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'
router.group(() => {
router.get('posts', () => {})
router.get('users', () => {})
router.get('payments', () => {})
}).use(middleware.userLocation())

ミドルウェアのパラメータ

名前付きミドルウェアコレクションに登録されたミドルウェアは、handleメソッドの引数として追加のパラメータを受け入れることができます。たとえば、authミドルウェアは認証ガードを設定オプションとして受け入れます。

type AuthGuards = 'web' | 'api'
export default class AuthMiddleware {
async handle(ctx, next, options: { guard: AuthGuards }) {
}
}

ミドルウェアをルートに割り当てる際に、使用するガードを指定できます。

import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'
router.get('payments', () => {}).use(
middleware.auth({ guard: 'web' })
)

依存性の注入

ミドルウェアクラスはIoCコンテナを使用してインスタンス化されるため、ミドルウェアのコンストラクタ内で依存関係を型指定できます。コンテナは依存関係を自動的に注入します。

リクエストIPからユーザーの場所を検索するためのGeoIpServiceクラスがある場合、@injectデコレータを使用してミドルウェアに注入できます。

app/services/geoip_service.ts
export default class GeoIpService {
async lookup(ipAddress: string) {
// 場所を検索して返す
}
}
import { inject } from '@adonisjs/core'
import { HttpContext } from '@adonisjs/core/http'
import { NextFn } from '@adonisjs/core/types/http'
import GeoIpService from '#services/geoip_service'
@inject()
export default class UserLocationMiddleware {
constructor(protected geoIpService: GeoIpService) {
}
async handle(ctx: HttpContext, next: NextFn) {
const ip = ctx.request.ip()
ctx.location = await this.geoIpService.lookup(ip)
}
}

ミドルウェアの実行フロー

AdonisJSのミドルウェアレイヤーは、責任連鎖デザインパターンを基にして構築されています。ミドルウェアには2つの実行フェーズがあります: ダウンストリームフェーズアップストリームフェーズ

  • ダウンストリームフェーズは、nextメソッドを呼び出す前に書かれたコードです。このフェーズでは、リクエストを処理します。
  • アップストリームフェーズは、nextメソッドを呼び出した後に書かれたコードです。このフェーズでは、レスポンスを検査したり、完全に変更したりできます。

ミドルウェアと例外処理

ミドルウェアパイプラインまたはルートハンドラが例外を発生させた場合、AdonisJSは自動的に例外をHTTPレスポンスに変換し、グローバルな例外ハンドラを使用して返します。

そのため、next関数の呼び出しをtry/catchステートメントでラップする必要はありません。また、自動的な例外処理により、next関数呼び出し後にアップストリームのロジックが常に実行されることが保証されます。

ミドルウェアからのレスポンスの変更

ミドルウェアのアップストリームフェーズでは、レスポンスのボディ、ヘッダー、ステータスコードを変更できます。これにより、ルートハンドラや他のミドルウェアによって設定された古いレスポンスが破棄されます。

レスポンスを変更する前に、正しいレスポンスタイプを扱っていることを確認してください。以下は、Responseクラスのレスポンスタイプのリストです。

  • 標準レスポンスは、response.sendメソッドを使用してデータ値を送信することを意味します。値はArrayObjectStringBoolean、またはBufferのいずれかです。
  • ストリーミングレスポンスは、response.streamメソッドを使用してストリームをレスポンスソケットにパイプすることを意味します。
  • ファイルダウンロードレスポンスは、response.downloadメソッドを使用してファイルをダウンロードすることを意味します。

特定のレスポンスに基づいて、特定のレスポンスプロパティにアクセスできる/できない場合があります。

標準レスポンスの扱い

標準レスポンスを変更する場合は、response.contentプロパティを使用してアクセスできます。まず、contentが存在するかどうかを確認してください。

import { HttpContext } from '@adonisjs/core/http'
import { NextFn } from '@adonisjs/core/types/http'
export default class {
async handle({ response }: HttpContext, next: NextFn) {
await next()
if (response.hasContent) {
console.log(response.content)
console.log(typeof response.content)
response.send(newResponse)
}
}
}

ストリーミングレスポンスの扱い

response.streamメソッドを使用して設定されたレスポンスストリームは、直ちにアウトゴーイングHTTPレスポンスにパイプされません。代わりに、AdonisJSはルートハンドラとミドルウェアパイプラインの処理が完了するのを待ちます。

その結果、ミドルウェア内では、既存のストリームを新しいストリームで置き換えたり、ストリームを監視するためのイベントハンドラを定義したりできます。

import { HttpContext } from '@adonisjs/core/http'
import { NextFn } from '@adonisjs/core/types/http'
export default class {
async handle({ response }: HttpContext, next: NextFn) {
await next()
if (response.hasStream) {
response.outgoingStream.on('data', (chunk) => {
console.log(chunk)
})
}
}
}

ファイルのダウンロード処理

response.downloadresponse.attachmentメソッドを使用して行われるファイルのダウンロード処理は、ルートハンドラとミドルウェアパイプラインが完了するまでダウンロード処理を遅延させます。

そのため、ミドルウェア内では、ダウンロードするファイルのパスを置き換えることができます。

import { HttpContext } from '@adonisjs/core/http'
import { NextFn } from '@adonisjs/core/types/http'
export default class {
async handle({ response }: HttpContext, next: NextFn) {
await next()
if (response.hasFileToStream) {
console.log(response.fileToStream.generateEtag)
console.log(response.fileToStream.path)
}
}
}

ミドルウェアクラスのテスト

ミドルウェアをクラスとして作成することで、ミドルウェアを独立してテストすることが容易になります(ミドルウェアの単体テスト)。ミドルウェアをテストするためには、いくつかの異なる方法があります。利用可能なすべてのオプションを見てみましょう。

もっとも簡単なオプションは、ミドルウェアクラスの新しいインスタンスを作成し、HTTPコンテキストとnextコールバック関数を使用してhandleメソッドを呼び出すことです。

import testUtils from '@adonisjs/core/services/test_utils'
import GeoIpService from '#services/geoip_service'
import UserLocationMiddleware from '#middleware/user_location_middleware'
const middleware = new UserLocationMiddleware(
new GeoIpService()
)
const ctx = testUtils.createHttpContext()
await middleware.handle(ctx, () => {
console.log('Next function invoked')
})

testUtilsサービスは、AdonisJSアプリケーションが起動した後にのみ利用可能です。ただし、パッケージ内でミドルウェアをテストする場合は、HttpContextFactoryクラスを使用してアプリケーションを起動せずにダミーのHTTPコンテキストインスタンスを作成できます。

実際の例として、CORSミドルウェアのテストを参照してください。

import {
RequestFactory,
ResponseFactory,
HttpContextFactory
} from '@adonisjs/core/factories/http'
const request = new RequestFactory().create()
const response = new ResponseFactory().create()
const ctx = new HttpContextFactory()
.merge({ request, response })
.create()
await middleware.handle(ctx, () => {
console.log('Next function invoked')
})

サーバーパイプラインの使用

もし、あなたのミドルウェアが他のミドルウェアが先に実行されることを前提としている場合、server.pipelineメソッドを使用してミドルウェアのパイプラインを作成できます。

  • server.pipelineメソッドは、ミドルウェアクラスの配列を受け入れます。
  • クラスのインスタンスは、IoCコンテナを使用して作成されます。
  • 実行フローは、HTTPリクエスト中のミドルウェアの元の実行フローと同じです。
import testUtils from '@adonisjs/core/services/test_utils'
import server from '@adonisjs/core/services/server'
import UserLocationMiddleware from '#middleware/user_location_middleware'
const pipeline = server.pipeline([
UserLocationMiddleware
])
const ctx = testUtils.createHttpContext()
await pipeline.run(ctx)

pipeline.runメソッドを呼び出す前に、finalHandler関数とerrorHandler関数を定義できます。

  • finalHandlerは、すべてのミドルウェアが実行された後に実行されます。ただし、ミドルウェアのいずれかがnextメソッドを呼び出さずにチェーンを終了した場合、finalHandlerは実行されません。
  • errorHandlerは、ミドルウェアが例外を発生させた場合に実行されます。エラーハンドラが呼び出された後、アップストリームのフローが開始されます。
const ctx = testUtils.createHttpContext()
await pipeline
.finalHandler(() => {
console.log('all middleware called next')
console.log('the upstream logic starts from here')
})
.errorHandler((error) => {
console.log('an exception was raised')
console.log('the upstream logic starts from here')
})
.run(ctx)
console.log('pipeline executed')

serverサービスは、アプリケーションが起動した後に利用可能です。ただし、パッケージを作成している場合は、ServerFactoryを使用してアプリケーションを起動せずにServerクラスのインスタンスを作成できます。

import { ServerFactory } from '@adonisjs/core/factories/http'
const server = new ServerFactory().create()
const pipeline = server.pipeline([
UserLocationMiddleware
])