检测客户端中止请求
简介
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 服务器,其中包括以下功能
- 在 http://localhost:3000 接收请求,并以 3 秒的延迟响应
{ ok: true }
。 - 一个 onRequest 钩子,在接收到每个请求时触发。
- 当请求关闭时在钩子中触发的逻辑。
- 当关闭的请求属性
aborted
为真时发生的日志记录。
虽然 aborted
属性已被弃用,但 destroyed
不是合适的替代方案,如 Node.js 文档所建议。请求可能由于各种原因而被 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 })
})
在应用程序代码中添加此功能的好处是,您可以记录 Fastify 的详细信息,例如 reqId,这些详细信息在仅访问原始请求信息的较低级别代码中可能不可用。
测试
要测试此功能,您可以使用 Postman 等应用程序并在 3 秒内取消请求。或者,您可以使用 Node 发送一个 HTTP 请求,并在 3 秒前中止该请求的逻辑。示例
const controller = new AbortController();
const signal = controller.signal;
(async () => {
try {
const response = await fetch('http://localhost:3000', { signal });
const body = await response.text();
console.log(body);
} catch (error) {
console.error(error);
}
})();
setTimeout(() => {
controller.abort()
}, 1000);
使用任何一种方法,您都应该在请求被中止时看到 Fastify 日志出现。
结论
实现的细节因问题而异,但本指南的主要目标是展示 Fastify 生态系统中可以解决的一个非常具体的用例。
您可以监听请求关闭事件并确定请求是被中止还是已成功交付。您可以在 onRequest 钩子中或直接在单个路由中实现此解决方案。
此方法不会在互联网中断的情况下触发,此类检测需要额外的业务逻辑。如果您的后端应用程序逻辑存在缺陷,导致服务器崩溃,则可能会触发错误检测。clientErrorHandler
,无论默认还是自定义逻辑,都不打算处理这种情况,并且在客户端中止请求时不会触发。