Skip to content

Commit 7cbad58

Browse files
ShogunPandaFdawgsEomm
authored
feat: new onRequestAbort hook (fastify#4582)
* feat: Added onClientAbort hook. * test: Removed leftovers. * docs: Added client detection reliability disclaimer. * feat: Renamed hook. * feat: Handle errors and don't forward the reply. * chore: Updated docs. Co-authored-by: Frazer Smith <[email protected]> * test: apply PR review suggestions Co-authored-by: Manuel Spigolon <[email protected]> --------- Co-authored-by: Frazer Smith <[email protected]> Co-authored-by: Manuel Spigolon <[email protected]>
1 parent 2e9526e commit 7cbad58

File tree

12 files changed

+392
-6
lines changed

12 files changed

+392
-6
lines changed

docs/Reference/Hooks.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ are Request/Reply hooks and application hooks:
1919
- [onSend](#onsend)
2020
- [onResponse](#onresponse)
2121
- [onTimeout](#ontimeout)
22+
- [onRequestAbort](#onrequestabort)
2223
- [Manage Errors from a hook](#manage-errors-from-a-hook)
2324
- [Respond to a request from a hook](#respond-to-a-request-from-a-hook)
2425
- [Application Hooks](#application-hooks)
@@ -267,6 +268,26 @@ service (if the `connectionTimeout` property is set on the Fastify instance).
267268
The `onTimeout` hook is executed when a request is timed out and the HTTP socket
268269
has been hanged up. Therefore, you will not be able to send data to the client.
269270

271+
### onRequestAbort
272+
273+
```js
274+
fastify.addHook('onRequestAbort', (request, reply, done) => {
275+
// Some code
276+
done()
277+
})
278+
```
279+
Or `async/await`:
280+
```js
281+
fastify.addHook('onRequestAbort', async (request, reply) => {
282+
// Some code
283+
await asyncMethod()
284+
})
285+
```
286+
The `onRequestAbort` hook is executed when a client closes the connection before
287+
the entire request has been received. Therefore, you will not be able to send
288+
data to the client.
289+
290+
**Notice:** client abort detection is not completely reliable. See: [`Detecting-When-Clients-Abort.md`](../Guides/Detecting-When-Clients-Abort.md)
270291

271292
### Manage Errors from a hook
272293
If you get an error during the execution of your hook, just pass it to `done()`

fastify.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { Chain as LightMyRequestChain, InjectOptions, Response as LightMyRequest
1212
import { FastifyBodyParser, FastifyContentTypeParser, AddContentTypeParser, hasContentTypeParser, getDefaultJsonParser, ProtoAction, ConstructorAction } from './types/content-type-parser'
1313
import { FastifyContext, FastifyContextConfig } from './types/context'
1414
import { FastifyErrorCodes } from './types/errors'
15-
import { DoneFuncWithErrOrRes, HookHandlerDoneFunction, RequestPayload, onCloseAsyncHookHandler, onCloseHookHandler, onErrorAsyncHookHandler, onErrorHookHandler, onReadyAsyncHookHandler, onReadyHookHandler, onRegisterHookHandler, onRequestAsyncHookHandler, onRequestHookHandler, onResponseAsyncHookHandler, onResponseHookHandler, onRouteHookHandler, onSendAsyncHookHandler, onSendHookHandler, onTimeoutAsyncHookHandler, onTimeoutHookHandler, preHandlerAsyncHookHandler, preHandlerHookHandler, preParsingAsyncHookHandler, preParsingHookHandler, preSerializationAsyncHookHandler, preSerializationHookHandler, preValidationAsyncHookHandler, preValidationHookHandler } from './types/hooks'
15+
import { DoneFuncWithErrOrRes, HookHandlerDoneFunction, RequestPayload, onCloseAsyncHookHandler, onCloseHookHandler, onErrorAsyncHookHandler, onErrorHookHandler, onReadyAsyncHookHandler, onReadyHookHandler, onRegisterHookHandler, onRequestAsyncHookHandler, onRequestHookHandler, onResponseAsyncHookHandler, onResponseHookHandler, onRouteHookHandler, onSendAsyncHookHandler, onSendHookHandler, onTimeoutAsyncHookHandler, onTimeoutHookHandler, preHandlerAsyncHookHandler, preHandlerHookHandler, preParsingAsyncHookHandler, preParsingHookHandler, preSerializationAsyncHookHandler, preSerializationHookHandler, preValidationAsyncHookHandler, preValidationHookHandler, onRequestAbortHookHandler, onRequestAbortAsyncHookHandler } from './types/hooks'
1616
import { FastifyListenOptions, FastifyInstance, PrintRoutesOptions } from './types/instance'
1717
import { FastifyBaseLogger, FastifyLoggerInstance, FastifyLoggerOptions, PinoLoggerOptions, FastifyLogFn, LogLevel } from './types/logger'
1818
import { FastifyPluginCallback, FastifyPluginAsync, FastifyPluginOptions, FastifyPlugin } from './types/plugin'
@@ -179,7 +179,7 @@ declare namespace fastify {
179179
FastifyError, // '@fastify/error'
180180
FastifySchema, FastifySchemaCompiler, // './types/schema'
181181
HTTPMethods, RawServerBase, RawRequestDefaultExpression, RawReplyDefaultExpression, RawServerDefault, ContextConfigDefault, RequestBodyDefault, RequestQuerystringDefault, RequestParamsDefault, RequestHeadersDefault, // './types/utils'
182-
DoneFuncWithErrOrRes, HookHandlerDoneFunction, RequestPayload, onCloseAsyncHookHandler, onCloseHookHandler, onErrorAsyncHookHandler, onErrorHookHandler, onReadyAsyncHookHandler, onReadyHookHandler, onRegisterHookHandler, onRequestAsyncHookHandler, onRequestHookHandler, onResponseAsyncHookHandler, onResponseHookHandler, onRouteHookHandler, onSendAsyncHookHandler, onSendHookHandler, onTimeoutAsyncHookHandler, onTimeoutHookHandler, preHandlerAsyncHookHandler, preHandlerHookHandler, preParsingAsyncHookHandler, preParsingHookHandler, preSerializationAsyncHookHandler, preSerializationHookHandler, preValidationAsyncHookHandler, preValidationHookHandler, // './types/hooks'
182+
DoneFuncWithErrOrRes, HookHandlerDoneFunction, RequestPayload, onCloseAsyncHookHandler, onCloseHookHandler, onErrorAsyncHookHandler, onErrorHookHandler, onReadyAsyncHookHandler, onReadyHookHandler, onRegisterHookHandler, onRequestAsyncHookHandler, onRequestHookHandler, onResponseAsyncHookHandler, onResponseHookHandler, onRouteHookHandler, onSendAsyncHookHandler, onSendHookHandler, onTimeoutAsyncHookHandler, onTimeoutHookHandler, preHandlerAsyncHookHandler, preHandlerHookHandler, preParsingAsyncHookHandler, preParsingHookHandler, preSerializationAsyncHookHandler, preSerializationHookHandler, preValidationAsyncHookHandler, preValidationHookHandler, onRequestAbortHookHandler, onRequestAbortAsyncHookHandler, // './types/hooks'
183183
FastifyServerFactory, FastifyServerFactoryHandler, // './types/serverFactory'
184184
FastifyTypeProvider, FastifyTypeProviderDefault, // './types/type-provider'
185185
FastifyErrorCodes, // './types/errors'

fastify.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,10 @@ function fastify (options) {
588588
if (fn.constructor.name === 'AsyncFunction' && fn.length !== 0) {
589589
throw new errorCodes.FST_ERR_HOOK_INVALID_ASYNC_HANDLER()
590590
}
591+
} else if (name === 'onRequestAbort') {
592+
if (fn.constructor.name === 'AsyncFunction' && fn.length !== 1) {
593+
throw new errorCodes.FST_ERR_HOOK_INVALID_ASYNC_HANDLER()
594+
}
591595
} else {
592596
if (fn.constructor.name === 'AsyncFunction' && fn.length === 3) {
593597
throw new errorCodes.FST_ERR_HOOK_INVALID_ASYNC_HANDLER()

lib/context.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ function Context ({
4848
this.preHandler = null
4949
this.onResponse = null
5050
this.preSerialization = null
51+
this.onRequestAbort = null
5152
this.config = config
5253
this.errorHandler = errorHandler || server[kErrorHandler]
5354
this._middie = null

lib/hooks.js

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ const lifecycleHooks = [
1515
'preHandler',
1616
'onSend',
1717
'onResponse',
18-
'onError'
18+
'onError',
19+
'onRequestAbort'
1920
]
2021
const supportedHooks = lifecycleHooks.concat(applicationHooks)
2122
const {
@@ -46,6 +47,7 @@ function Hooks () {
4647
this.onRegister = []
4748
this.onReady = []
4849
this.onTimeout = []
50+
this.onRequestAbort = []
4951
}
5052

5153
Hooks.prototype.validate = function (hook, fn) {
@@ -74,6 +76,7 @@ function buildHooks (h) {
7476
hooks.onRoute = h.onRoute.slice()
7577
hooks.onRegister = h.onRegister.slice()
7678
hooks.onTimeout = h.onTimeout.slice()
79+
hooks.onRequestAbort = h.onRequestAbort.slice()
7780
hooks.onReady = []
7881
return hooks
7982
}
@@ -246,6 +249,42 @@ function onSendHookRunner (functions, request, reply, payload, cb) {
246249
next()
247250
}
248251

252+
function onRequestAbortHookRunner (functions, runner, request, cb) {
253+
let i = 0
254+
255+
function next (err) {
256+
if (err || i === functions.length) {
257+
cb(err, request)
258+
return
259+
}
260+
261+
let result
262+
try {
263+
result = runner(functions[i++], request, next)
264+
} catch (error) {
265+
next(error)
266+
return
267+
}
268+
if (result && typeof result.then === 'function') {
269+
result.then(handleResolve, handleReject)
270+
}
271+
}
272+
273+
function handleResolve () {
274+
next()
275+
}
276+
277+
function handleReject (err) {
278+
if (!err) {
279+
err = new FST_ERR_SEND_UNDEFINED_ERR()
280+
}
281+
282+
cb(err, request)
283+
}
284+
285+
next()
286+
}
287+
249288
function hookIterator (fn, request, reply, next) {
250289
if (reply.sent === true) return undefined
251290
return fn(request, reply, next)
@@ -256,6 +295,7 @@ module.exports = {
256295
buildHooks,
257296
hookRunner,
258297
onSendHookRunner,
298+
onRequestAbortHookRunner,
259299
hookIterator,
260300
hookRunnerApplication,
261301
lifecycleHooks,

lib/route.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
const FindMyWay = require('find-my-way')
44
const Context = require('./context')
55
const handleRequest = require('./handleRequest')
6-
const { hookRunner, hookIterator, lifecycleHooks } = require('./hooks')
6+
const { hookRunner, hookIterator, onRequestAbortHookRunner, lifecycleHooks } = require('./hooks')
77
const { supportedMethods } = require('./httpMethods')
88
const { normalizeSchema } = require('./schemas')
99
const { parseHeadOnSendHandlers } = require('./headRoute')
@@ -484,6 +484,20 @@ function buildRouting (options) {
484484
runPreParsing(null, request, reply)
485485
}
486486

487+
if (context.onRequestAbort !== null) {
488+
req.on('close', () => {
489+
/* istanbul ignore else */
490+
if (req.aborted) {
491+
onRequestAbortHookRunner(
492+
context.onRequestAbort,
493+
hookIterator,
494+
request,
495+
handleOnRequestAbortHooksErrors.bind(null, reply)
496+
)
497+
}
498+
})
499+
}
500+
487501
if (context.onTimeout !== null) {
488502
if (!request.raw.socket._meta) {
489503
request.raw.socket.on('timeout', handleTimeout)
@@ -493,6 +507,12 @@ function buildRouting (options) {
493507
}
494508
}
495509

510+
function handleOnRequestAbortHooksErrors (reply, err) {
511+
if (err) {
512+
reply.log.error({ err }, 'onRequestAborted hook failed')
513+
}
514+
}
515+
496516
function handleTimeout () {
497517
const { context, request, reply } = this._meta
498518
hookRunner(

test/hooks-async.test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,18 @@ test('preHandler respond with a stream', t => {
584584
})
585585

586586
test('Should log a warning if is an async function with `done`', t => {
587+
t.test('2 arguments', t => {
588+
t.plan(2)
589+
const fastify = Fastify()
590+
591+
try {
592+
fastify.addHook('onRequestAbort', async (req, done) => {})
593+
} catch (e) {
594+
t.ok(e.code, 'FST_ERR_HOOK_INVALID_ASYNC_HANDLER')
595+
t.ok(e.message === 'Async function has too many arguments. Async hooks should not use the \'done\' argument.')
596+
}
597+
})
598+
587599
t.test('3 arguments', t => {
588600
t.plan(2)
589601
const fastify = Fastify()

0 commit comments

Comments
 (0)