strong-tie/inbound-calls
0
1<h1 align="center">Fastify</h1>2 3# Detecting When Clients Abort4 5## Introduction6 7Fastify provides request events to trigger at certain points in a request's8lifecycle. However, there isn't a built-in mechanism to9detect unintentional client disconnection scenarios such as when the client's10internet connection is interrupted. This guide covers methods to detect if11and when a client intentionally aborts a request.12 13Keep in mind, Fastify's `clientErrorHandler` is not designed to detect when a14client aborts a request. This works in the same way as the standard Node HTTP15module, which triggers the `clientError` event when there is a bad request or16exceedingly large header data. When a client aborts a request, there is no17error on the socket and the `clientErrorHandler` will not be triggered.18 19## Solution20 21### Overview22 23The proposed solution is a possible way of detecting when a client24intentionally aborts a request, such as when a browser is closed or the HTTP25request is aborted from your client application. If there is an error in your26application code that results in the server crashing, you may require27additional logic to avoid a false abort detection.28 29The goal here is to detect when a client intentionally aborts a connection30so your application logic can proceed accordingly. This can be useful for31logging purposes or halting business logic.32 33### Hands-on34 35Say we have the following base server set up:36 37```js38import Fastify from 'fastify';39 40const sleep = async (time) => {41 return await new Promise(resolve => setTimeout(resolve, time || 1000));42}43 44const app = Fastify({45 logger: {46 transport: {47 target: 'pino-pretty',48 options: {49 translateTime: 'HH:MM:ss Z',50 ignore: 'pid,hostname',51 },52 },53 },54})55 56app.addHook('onRequest', async (request, reply) => {57 request.raw.on('close', () => {58 if (request.raw.aborted) {59 app.log.info('request closed')60 }61 })62})63 64app.get('/', async (request, reply) => {65 await sleep(3000)66 reply.code(200).send({ ok: true })67})68 69const start = async () => {70 try {71 await app.listen({ port: 3000 })72 } catch (err) {73 app.log.error(err)74 process.exit(1)75 }76}77 78start()79```80 81Our code is setting up a Fastify server which includes the following82functionality:83 84- Accepting requests at http://localhost:3000, with a 3 second delayed response85of `{ ok: true }`.86- An onRequest hook that triggers when every request is received.87- Logic that triggers in the hook when the request is closed.88- Logging that occurs when the closed request property `aborted` is true.89 90Whilst the `aborted` property has been deprecated, `destroyed` is not a91suitable replacement as the92[Node.js documentation suggests](https://nodejs.org/api/http.html#requestaborted).93A request can be `destroyed` for various reasons, such as when the server closes94the connection. The `aborted` property is still the most reliable way to detect95when a client intentionally aborts a request.96 97You can also perform this logic outside of a hook, directly in a specific route.98 99```js100app.get('/', async (request, reply) => {101 request.raw.on('close', () => {102 if (request.raw.aborted) {103 app.log.info('request closed')104 }105 })106 await sleep(3000)107 reply.code(200).send({ ok: true })108})109```110 111At any point in your business logic, you can check if the request has been112aborted and perform alternative actions.113 114```js115app.get('/', async (request, reply) => {116 await sleep(3000)117 if (request.raw.aborted) {118 // do something here119 }120 await sleep(3000)121 reply.code(200).send({ ok: true })122})123```124 125A benefit to adding this in your application code is that you can log Fastify126details such as the reqId, which may be unavailable in lower-level code that127only has access to the raw request information.128 129### Testing130 131To test this functionality you can use an app like Postman and cancel your132request within 3 seconds. Alternatively, you can use Node to send an HTTP133request with logic to abort the request before 3 seconds. Example:134 135```js136const controller = new AbortController();137const signal = controller.signal;138 139(async () => {140 try {141 const response = await fetch('http://localhost:3000', { signal });142 const body = await response.text();143 console.log(body);144 } catch (error) {145 console.error(error);146 }147})();148 149setTimeout(() => {150 controller.abort()151}, 1000);152```153 154With either approach, you should see the Fastify log appear at the moment the155request is aborted.156 157## Conclusion158 159Specifics of the implementation will vary from one problem to another, but the160main goal of this guide was to show a very specific use case of an issue that161could be solved within Fastify's ecosystem.162 163You can listen to the request close event and determine if the request was164aborted or if it was successfully delivered. You can implement this solution165in an onRequest hook or directly in an individual route.166 167This approach will not trigger in the event of internet disruption, and such168detection would require additional business logic. If you have flawed backend169application logic that results in a server crash, then you could trigger a170false detection. The `clientErrorHandler`, either by default or with custom171logic, is not intended to handle this scenario and will not trigger when the172client aborts a request.173 