Skip to content

Commit 6f4842d

Browse files
Docs: Add guide about detecting client abort (fastify#4518)
* docs: add guide about client abort * fix lint errors * fix line break * A guide to table of contents * grammar updates Co-authored-by: Frazer Smith <[email protected]> * grammar updates Co-authored-by: Frazer Smith <[email protected]> * grammar updates Co-authored-by: Frazer Smith <[email protected]> * grammar updates Co-authored-by: Frazer Smith <[email protected]> * Use fetch for testing example Co-authored-by: Frazer Smith <[email protected]>
1 parent d34f81b commit 6f4842d

File tree

2 files changed

+182
-0
lines changed

2 files changed

+182
-0
lines changed
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
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.

docs/Guides/Index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ This table of contents is in alphabetical order.
1515
met in your application. This guide focuses on solving the problem using
1616
[`Hooks`](../Reference/Hooks.md), [`Decorators`](../Reference/Decorators.md),
1717
and [`Plugins`](../Reference/Plugins.md).
18+
+ [Detecting When Clients Abort](./Detecting-When-Clients-Abort.md): A
19+
practical guide on detecting if and when a client aborts a request.
1820
+ [Ecosystem](./Ecosystem.md): Lists all core plugins and many known community
1921
plugins.
2022
+ [Fluent Schema](./Fluent-Schema.md): Shows how writing JSON Schema can be

0 commit comments

Comments
 (0)