|
| 1 | +<h1 align="center">Fastify</h1> |
| 2 | + |
| 3 | +# Detecting When Clients Abort |
| 4 | + |
| 5 | +## Introduction |
| 6 | + |
| 7 | +Fastify provides request events to trigger at certain points in a request's |
| 8 | +lifecycle. However, there isn't a mechanism built-in to |
| 9 | +detect unintentional client disconnection scenarios such as when the client's |
| 10 | +internet connection is interrupted. This guide covers methods to detect if |
| 11 | +and when a client intentionally aborts a request. |
| 12 | + |
| 13 | +Keep in mind, Fastify's clientErrorHandler is not designed to detect when a |
| 14 | +client aborts a request. This works in the same way as the standard Node HTTP |
| 15 | +module, which triggers the clientError event when there is a bad request or |
| 16 | +exceedingly large header data. When a client aborts a request, there is no |
| 17 | +error on the socket and the clientErrorHandler will not be triggered. |
| 18 | + |
| 19 | +## Solution |
| 20 | + |
| 21 | +### Overview |
| 22 | + |
| 23 | +The proposed solution is a possible way of detecting when a client |
| 24 | +intentionally aborts a request, such as when a browser is closed or the HTTP |
| 25 | +request is aborted from your client application. If there is an error in your |
| 26 | +application code that results in the server crashing, you may require |
| 27 | +additional logic to avoid a false abort detection. |
| 28 | + |
| 29 | +The goal here is to detect when a client intentionally aborts a connection |
| 30 | +so your application logic can proceed accordingly. This can be useful for |
| 31 | +logging purposes or halting business logic. |
| 32 | + |
| 33 | +### Hands-on |
| 34 | + |
| 35 | +For this sample solution, we'll be using the following: |
| 36 | + |
| 37 | +- `node.js v18.12.1` |
| 38 | +- `npm 8.19.2` |
| 39 | +- `fastify 4.11.0` |
| 40 | + |
| 41 | +Say we have the following base server set up: |
| 42 | + |
| 43 | +```js |
| 44 | +import Fastify from 'fastify'; |
| 45 | + |
| 46 | +const sleep = async (time) => { |
| 47 | + return await new Promise(resolve => setTimeout(resolve, time || 1000)); |
| 48 | +} |
| 49 | + |
| 50 | +const app = Fastify({ |
| 51 | + logger: { |
| 52 | + transport: { |
| 53 | + target: 'pino-pretty', |
| 54 | + options: { |
| 55 | + translateTime: 'HH:MM:ss Z', |
| 56 | + ignore: 'pid,hostname', |
| 57 | + }, |
| 58 | + }, |
| 59 | + }, |
| 60 | +}) |
| 61 | + |
| 62 | +app.addHook('onRequest', async (request, reply) => { |
| 63 | + request.raw.on('close', () => { |
| 64 | + if (request.raw.aborted) { |
| 65 | + app.log.info('request closed') |
| 66 | + } |
| 67 | + }) |
| 68 | +}) |
| 69 | + |
| 70 | +app.get('/', async (request, reply) => { |
| 71 | + await sleep(3000) |
| 72 | + reply.code(200).send({ ok: true }) |
| 73 | +}) |
| 74 | + |
| 75 | +const start = async () => { |
| 76 | + try { |
| 77 | + await app.listen({ port: 3000 }) |
| 78 | + } catch (err) { |
| 79 | + app.log.error(err) |
| 80 | + process.exit(1) |
| 81 | + } |
| 82 | +} |
| 83 | + |
| 84 | +start() |
| 85 | +``` |
| 86 | + |
| 87 | +Our code is setting up a Fastify server which includes the following |
| 88 | +functionality: |
| 89 | + |
| 90 | +- Accepting requests at http://localhost:3000, with a 3 second delayed response |
| 91 | +of { ok: true }. |
| 92 | +- An onRequest hook that triggers when every request is received. |
| 93 | +- Logic that triggers in the hook when the request is closed. |
| 94 | +- Logging that occurs when the closed request attribute 'aborted' is true. |
| 95 | + |
| 96 | +In the request close event, you should examine the diff between a successful |
| 97 | +request and one aborted by the client to determine the best attribute for your |
| 98 | +use case. There are many other attributes on a request that will differ between |
| 99 | +a successfully closed request and one that has been aborted by the client. |
| 100 | +Examples of such attributes include: |
| 101 | + |
| 102 | +- destroyed |
| 103 | +- errors |
| 104 | + |
| 105 | +You can also perform this logic outside of a hook, directly in a specific route. |
| 106 | + |
| 107 | +```js |
| 108 | +app.get('/', async (request, reply) => { |
| 109 | + request.raw.on('close', () => { |
| 110 | + if (request.raw.aborted) { |
| 111 | + app.log.info('request closed') |
| 112 | + } |
| 113 | + }) |
| 114 | + await sleep(3000) |
| 115 | + reply.code(200).send({ ok: true }) |
| 116 | +}) |
| 117 | +``` |
| 118 | + |
| 119 | +At any point in your business logic, you can check if the request has been |
| 120 | +aborted and perform alternative actions. |
| 121 | + |
| 122 | +```js |
| 123 | +app.get('/', async (request, reply) => { |
| 124 | + await sleep(3000) |
| 125 | + if (request.raw.aborted) { |
| 126 | + // do something here |
| 127 | + } |
| 128 | + await sleep(3000) |
| 129 | + reply.code(200).send({ ok: true }) |
| 130 | +}) |
| 131 | +``` |
| 132 | + |
| 133 | +A benefit to adding this in your application code is that you can log Fastify |
| 134 | +details such as the reqId, which may be unavailable in lower-level code that |
| 135 | +only has access to the raw request information. |
| 136 | + |
| 137 | +### Testing |
| 138 | + |
| 139 | +To test this functionality you can use an app like Postman and cancel your |
| 140 | +request within 3 seconds. Alternatively, you can use Node to send an HTTP |
| 141 | +request with logic to abort the request before 3 seconds. Example: |
| 142 | + |
| 143 | +```js |
| 144 | +const controller = new AbortController(); |
| 145 | +const signal = controller.signal; |
| 146 | + |
| 147 | +(async () => { |
| 148 | + try { |
| 149 | + const response = await fetch('http://localhost:3000', { signal }); |
| 150 | + const body = await response.text(); |
| 151 | + console.log(body); |
| 152 | + } catch (error) { |
| 153 | + console.error(error); |
| 154 | + } |
| 155 | +})(); |
| 156 | + |
| 157 | +setTimeout(() => { |
| 158 | + controller.abort() |
| 159 | +}, 1000); |
| 160 | +``` |
| 161 | + |
| 162 | +With either approach, you should see the Fastify log appear at the moment the |
| 163 | +request is aborted. |
| 164 | + |
| 165 | +## Conclusion |
| 166 | + |
| 167 | +Specifics of the implementation will vary from one problem to another, but the |
| 168 | +main goal of this guide was to show a very specific use case of an issue that |
| 169 | +could be solved within Fastify's ecosystem. |
| 170 | + |
| 171 | +You can listen to the request close event and determine if the request was |
| 172 | +aborted or if it was successfully delivered. You can implement this solution |
| 173 | +in an onRequest hook or directly in an individual route. |
| 174 | + |
| 175 | +This approach will not trigger in the event of internet disruption, and such |
| 176 | +detection would require additional business logic. If you have flawed backend |
| 177 | +application logic that results in a server crash, then you could trigger a |
| 178 | +false detection. The clientErrorHandler, either by default or with custom |
| 179 | +logic, is not intended to handle this scenario and will not trigger when the |
| 180 | +client aborts a request. |
0 commit comments