クライアントが中断した場合の検出
はじめに
Fastifyは、リクエストのライフサイクルの特定の時点でトリガーするためのリクエストイベントを提供します。ただし、クライアントのインターネット接続が中断した場合など、意図しないクライアントの切断シナリオを検出する組み込みのメカニズムはありません。このガイドでは、クライアントが意図的にリクエストを中断した場合を検出する方法について説明します。
FastifyのclientErrorHandler
は、クライアントがリクエストを中断した場合を検出するように設計されていないことに注意してください。これは、標準のNode HTTPモジュールと同じように動作し、不正なリクエストまたは非常に大きなヘッダーデータがある場合にclientError
イベントをトリガーします。クライアントがリクエストを中断した場合、ソケットにエラーはなく、clientErrorHandler
はトリガーされません。
解決策
概要
提案された解決策は、ブラウザが閉じられたり、クライアントアプリケーションからHTTPリクエストが中断されたりした場合など、クライアントが意図的にリクエストを中断した場合を検出する可能な方法です。サーバーがクラッシュするようなアプリケーションコードにエラーがある場合は、誤った中断検出を回避するための追加のロジックが必要になる場合があります。
ここでの目標は、クライアントが意図的に接続を中断した場合を検出し、アプリケーションロジックがそれに応じて進行できるようにすることです。これは、ロギング目的やビジネスロジックの停止に役立ちます。
実践
次のベースサーバーがセットアップされているとします。
import Fastify from 'fastify';
const sleep = async (time) => {
return await new Promise(resolve => setTimeout(resolve, time || 1000));
}
const app = Fastify({
logger: {
transport: {
target: 'pino-pretty',
options: {
translateTime: 'HH:MM:ss Z',
ignore: 'pid,hostname',
},
},
},
})
app.addHook('onRequest', async (request, reply) => {
request.raw.on('close', () => {
if (request.raw.aborted) {
app.log.info('request closed')
}
})
})
app.get('/', async (request, reply) => {
await sleep(3000)
reply.code(200).send({ ok: true })
})
const start = async () => {
try {
await app.listen({ port: 3000 })
} catch (err) {
app.log.error(err)
process.exit(1)
}
}
start()
私たちのコードは、次の機能を含むFastifyサーバーをセットアップしています。
- https://#:3000でのリクエストの受け入れ、および
{ ok: true }
の3秒遅延応答。 - すべてのリクエストを受信したときにトリガーされるonRequestフック。
- リクエストが閉じられたときにフックでトリガーされるロジック。
- 閉じられたリクエストプロパティ
aborted
がtrueの場合に発生するロギング。
aborted
プロパティは非推奨になりましたが、Node.jsのドキュメントが示唆しているように、destroyed
は適切な代替ではありません。リクエストは、サーバーが接続を閉じた場合など、さまざまな理由でdestroyed
になる可能性があります。aborted
プロパティは、クライアントが意図的にリクエストを中断した場合を検出する最も信頼性の高い方法です。
このロジックは、フックの外で、特定のルートで直接実行することもできます。
app.get('/', async (request, reply) => {
request.raw.on('close', () => {
if (request.raw.aborted) {
app.log.info('request closed')
}
})
await sleep(3000)
reply.code(200).send({ ok: true })
})
ビジネスロジックのどの時点でも、リクエストが中断されたかどうかを確認し、代替アクションを実行できます。
app.get('/', async (request, reply) => {
await sleep(3000)
if (request.raw.aborted) {
// do something here
}
await sleep(3000)
reply.code(200).send({ ok: true })
})
このロジックをアプリケーションコードに追加することの利点は、生のリクエスト情報のみにアクセスできる低レベルのコードでは利用できない可能性があるreqIdなどのFastifyの詳細をログに記録できることです。
テスト
この機能をテストするには、Postmanなどのアプリを使用し、3秒以内にリクエストをキャンセルできます。または、Nodeを使用して、3秒前にリクエストを中止するロジックでHTTPリクエストを送信することもできます。例
const controller = new AbortController();
const signal = controller.signal;
(async () => {
try {
const response = await fetch('https://#:3000', { signal });
const body = await response.text();
console.log(body);
} catch (error) {
console.error(error);
}
})();
setTimeout(() => {
controller.abort()
}, 1000);
どちらの方法でも、リクエストが中断された瞬間にFastifyログが表示されるはずです。
結論
実装の詳細は問題によって異なりますが、このガイドの主な目標は、Fastifyのエコシステム内で解決できる可能性のある問題の非常に具体的なユースケースを示すことでした。
リクエストのクローズイベントをリッスンし、リクエストが中断されたか、正常に配信されたかを判断できます。このソリューションは、onRequestフックで、または個々のルートで直接実装できます。
このアプローチは、インターネットの中断が発生した場合にトリガーされることはありません。そのような検出には、追加のビジネスロジックが必要です。サーバーのクラッシュを引き起こすような欠陥のあるバックエンドアプリケーションロジックがある場合は、誤った検出をトリガーする可能性があります。デフォルトまたはカスタムロジックを使用したclientErrorHandler
は、このシナリオを処理することを意図しておらず、クライアントがリクエストを中断してもトリガーされません。