Skip to content

Commit 55dd5b2

Browse files
authored
Add preClose opt + merge close and preClose hook (#261)
* Add preClose option Also commenting out failing test from before these changes, so preCommit hook passes * Rm onClose hook and handle that logic in preClose * Make preClose top level option * Add preClose opt to readme * Put noHandle back in its original spot * Add back test * Clarify preClose can be an async function * Add preClose to types * Reset plan count to original (need to force this through the precommit because it fails locally, but I think it will pass the CI) * Add type tests for preClose opt
1 parent ec0e7ae commit 55dd5b2

File tree

5 files changed

+109
-7
lines changed

5 files changed

+109
-7
lines changed

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,29 @@ fastify.listen({ port: 3000 }, err => {
233233
```
234234

235235
Note: Fastify's `onError` and error handlers registered by `setErrorHandler` will still be called for errors encountered *before* the websocket connection is established. This means errors thrown by `onRequest` hooks, `preValidation` handlers, and hooks registered by plugins will use the normal error handling mechanisms in Fastify. Once the websocket is established and your websocket route handler is called, `fastify-websocket`'s `errorHandler` takes over.
236+
237+
### Custom preClose hook:
238+
239+
By default, all ws connections are closed when the server closes. If you wish to modify this behaviour, you can pass your own `preClose` function.
240+
241+
Note that `preClose` is responsible for closing all connections and closing the websocket server.
242+
243+
```js
244+
const fastify = require('fastify')()
245+
246+
fastify.register(require('@fastify/websocket'), {
247+
preClose: (done) => { // Note: can also use async style, without done-callback
248+
const server = this.websocketServer
249+
250+
for (const connection of server.clients) {
251+
connection.close(1001, 'WS server is going offline in custom manner, sending a code + message')
252+
}
253+
254+
server.close(done)
255+
}
256+
})
257+
```
258+
236259
## Options
237260

238261
`@fastify/websocket` accept these options for [`ws`](https://github.com/websockets/ws/blob/master/doc/ws.md#new-websocketserveroptions-callback) :

index.js

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ function fastifyWebsocket (fastify, opts, next) {
1919
errorHandler = opts.errorHandler
2020
}
2121

22+
let preClose = defaultPreClose
23+
if (opts && opts.preClose) {
24+
if (typeof opts.preClose !== 'function') {
25+
return next(new Error('invalid preClose function'))
26+
}
27+
28+
preClose = opts.preClose
29+
}
30+
2231
if (opts.options && opts.options.noServer) {
2332
return next(new Error("fastify-websocket doesn't support the ws noServer option. If you want to create a websocket server detatched from fastify, use the ws library directly."))
2433
}
@@ -143,25 +152,23 @@ function fastifyWebsocket (fastify, opts, next) {
143152
}
144153
})
145154

146-
fastify.addHook('onClose', close)
147-
148155
// Fastify is missing a pre-close event, or the ability to
149156
// add a hook before the server.close call. We need to resort
150157
// to monkeypatching for now.
151-
fastify.addHook('preClose', function (done) {
158+
fastify.addHook('preClose', preClose)
159+
160+
function defaultPreClose (done) {
152161
const server = this.websocketServer
153162
if (server.clients) {
154163
for (const client of server.clients) {
155164
client.close()
156165
}
157166
}
158167
fastify.server.removeListener('upgrade', onUpgrade)
159-
done()
160-
})
161168

162-
function close (fastify, done) {
163-
const server = fastify.websocketServer
164169
server.close(done)
170+
171+
done()
165172
}
166173

167174
function noHandle (connection, rawRequest) {

test/base.test.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,74 @@ test('Should be able to pass custom connectionOptions to createWebSocketStream',
351351
await p
352352
})
353353

354+
test('Should be able to pass preClose option to override default', async (t) => {
355+
t.plan(3)
356+
357+
const fastify = Fastify()
358+
359+
const preClose = (done) => {
360+
t.pass('Custom preclose successfully called')
361+
362+
for (const connection of fastify.websocketServer.clients) {
363+
connection.close()
364+
}
365+
done()
366+
}
367+
368+
await fastify.register(fastifyWebsocket, { preClose })
369+
370+
fastify.get('/', { websocket: true }, (connection) => {
371+
connection.setEncoding('utf8')
372+
t.teardown(() => connection.destroy())
373+
374+
connection.once('data', (chunk) => {
375+
t.equal(chunk, 'hello server')
376+
connection.write('hello client')
377+
connection.end()
378+
})
379+
})
380+
381+
await fastify.listen({ port: 0 })
382+
383+
const ws = new WebSocket('ws://localhost:' + fastify.server.address().port)
384+
const client = WebSocket.createWebSocketStream(ws, { encoding: 'utf8' })
385+
t.teardown(() => client.destroy())
386+
387+
client.setEncoding('utf8')
388+
client.write('hello server')
389+
390+
const [chunk] = await once(client, 'data')
391+
t.equal(chunk, 'hello client')
392+
client.end()
393+
394+
await fastify.close()
395+
})
396+
397+
test('Should fail if custom preClose is not a function', async (t) => {
398+
t.plan(2)
399+
400+
const fastify = Fastify()
401+
t.teardown(() => fastify.close())
402+
403+
const preClose = 'Not a function'
404+
405+
try {
406+
await fastify.register(fastifyWebsocket, { preClose })
407+
} catch (err) {
408+
t.equal(err.message, 'invalid preClose function')
409+
}
410+
411+
fastify.get('/', { websocket: true }, (connection) => {
412+
t.teardown(() => connection.destroy())
413+
})
414+
415+
try {
416+
await fastify.listen({ port: 0 })
417+
} catch (err) {
418+
t.equal(err.message, 'invalid preClose function')
419+
}
420+
})
421+
354422
test('Should gracefully close with a connected client', async (t) => {
355423
t.plan(2)
356424

types/index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as fastify from 'fastify';
55
import WebSocket from 'ws';
66
import { Duplex, DuplexOptions } from 'stream';
77
import { FastifyReply } from 'fastify/types/reply';
8+
import { preCloseHookHandler, preCloseAsyncHookHandler } from 'fastify/types/hooks';
89
import { RouteGenericInterface } from 'fastify/types/route';
910

1011
interface WebsocketRouteOptions<
@@ -88,6 +89,7 @@ declare namespace fastifyWebsocket {
8889
errorHandler?: (this: FastifyInstance, error: Error, connection: SocketStream, request: FastifyRequest, reply: FastifyReply) => void;
8990
options?: WebSocketServerOptions;
9091
connectionOptions?: DuplexOptions;
92+
preClose?: preCloseHookHandler | preCloseAsyncHookHandler;
9193
}
9294

9395
export interface RouteOptions<

types/index.test-d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ app.register(wsPlugin, {
2222
}
2323
});
2424
app.register(wsPlugin, { options: { perMessageDeflate: true } });
25+
app.register(wsPlugin, { preClose: function syncPreclose() {} });
26+
app.register(wsPlugin, { preClose: async function asyncPreclose(){} });
2527

2628
app.get('/websockets-via-inferrence', { websocket: true }, async function (connection, request) {
2729
expectType<FastifyInstance>(this);

0 commit comments

Comments
 (0)