Skip to content

Commit a0ec207

Browse files
authored
Ensure that hooks and error handlers can send standard HTTP replies (#175)
1 parent 8ffab91 commit a0ec207

File tree

3 files changed

+135
-2
lines changed

3 files changed

+135
-2
lines changed

README.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,29 @@ fastify.get('/*', { websocket: true }, (connection, request) => {
101101
})
102102
})
103103
```
104+
### Using hooks
105+
106+
Routes registered with `fastify-websocket` respect the Fastify plugin encapsulation contexts, and so will run any hooks that have been registered. This means the same route hooks you might use for authentication or error handling of plain old HTTP handlers will apply to websocket handlers as well.
107+
108+
```js
109+
fastify.addHook('preValidation', async (request, reply) => {
110+
// check if the request is authenticated
111+
if (!request.isAuthenticated()) {
112+
await reply.code(401).send("not authenticated");
113+
}
114+
})
115+
fastify.get('/', { websocket: true }, (connection, req) => {
116+
// the connection will only be opened for authenticated incoming requests
117+
connection.socket.on('message', message => {
118+
// ...
119+
})
120+
})
121+
```
104122

105123
**NB**
106124
This plugin uses the same router as the `fastify` instance, this has a few implications to take into account:
107-
- Websocket route handlers follow the usual `fastify` request lifecycle.
125+
- Websocket route handlers follow the usual `fastify` request lifecycle, which means hooks, error handlers, and decorators all work the same way as other route handlers.
108126
- You can access the fastify server via `this` in your handlers
109-
- You can access the fastify request decorations via the `req` object your handlers
110127
- When using `fastify-websocket`, it needs to be registered before all routes in order to be able to intercept websocket connections to existing routes and close the connection on non-websocket routes.
111128

112129
```js

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ function fastifyWebsocket (fastify, opts, next) {
4747
})
4848
} else {
4949
const rawResponse = new ServerResponse(rawRequest)
50+
rawResponse.assignSocket(socket)
5051
fastify.routing(rawRequest, rawResponse)
5152
}
5253
})

test/hooks.js

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,65 @@ test('Should run onError hook before handler is executed (error thrown in onRequ
9999
})
100100
})
101101

102+
test('Should run onError hook before handler is executed (error thrown in preValidation hook)', t => {
103+
t.plan(3)
104+
const fastify = Fastify()
105+
106+
t.teardown(() => fastify.close())
107+
108+
fastify.register(fastifyWebsocket)
109+
110+
fastify.addHook('preValidation', async (request, reply) => {
111+
await Promise.resolve()
112+
throw new Error('Fail')
113+
})
114+
115+
fastify.addHook('onError', async (request, reply) => t.ok('called', 'onError'))
116+
117+
fastify.get('/echo', { websocket: true }, (conn, request) => {
118+
t.fail()
119+
})
120+
121+
fastify.listen(0, function (err) {
122+
t.error(err)
123+
const ws = new WebSocket('ws://localhost:' + (fastify.server.address()).port + '/echo')
124+
const client = WebSocket.createWebSocketStream(ws, { encoding: 'utf8' })
125+
t.teardown(client.destroy.bind(client))
126+
ws.on('close', code => t.equal(code, 1006))
127+
})
128+
})
129+
130+
test('onError hooks can send a reply and prevent hijacking', t => {
131+
t.plan(3)
132+
const fastify = Fastify()
133+
134+
t.teardown(() => fastify.close())
135+
136+
fastify.register(fastifyWebsocket)
137+
138+
fastify.addHook('preValidation', async (request, reply) => {
139+
await Promise.resolve()
140+
throw new Error('Fail')
141+
})
142+
143+
fastify.addHook('onError', async (request, reply) => {
144+
t.ok('called', 'onError')
145+
await reply.code(404).send('there was an error')
146+
})
147+
148+
fastify.get('/echo', { websocket: true }, (conn, request) => {
149+
t.fail()
150+
})
151+
152+
fastify.listen(0, function (err) {
153+
t.error(err)
154+
const ws = new WebSocket('ws://localhost:' + (fastify.server.address()).port + '/echo')
155+
const client = WebSocket.createWebSocketStream(ws, { encoding: 'utf8' })
156+
t.teardown(client.destroy.bind(client))
157+
ws.on('close', code => t.equal(code, 1006))
158+
})
159+
})
160+
102161
test('Should not run onError hook if reply was already hijacked (error thrown in websocket handler)', t => {
103162
t.plan(2)
104163
const fastify = Fastify()
@@ -172,6 +231,8 @@ test('Should not hijack reply for a normal http request in the internal onError
172231
const port = fastify.server.address().port
173232

174233
const httpClient = net.createConnection({ port: port }, () => {
234+
t.teardown(httpClient.destroy.bind(httpClient))
235+
175236
httpClient.write('GET / HTTP/1.1\r\n\r\n')
176237
httpClient.once('data', data => {
177238
t.match(data.toString(), /Fail/i)
@@ -221,3 +282,57 @@ test('Should run async hooks and still deliver quickly sent messages', (t) => {
221282
})
222283
})
223284
})
285+
286+
test('Should not hijack reply for an normal request to a websocket route that is sent a normal HTTP response in a hook', t => {
287+
t.plan(2)
288+
const fastify = Fastify()
289+
t.teardown(() => fastify.close())
290+
291+
fastify.register(fastifyWebsocket)
292+
fastify.addHook('preValidation', async (request, reply) => {
293+
await Promise.resolve()
294+
await reply.code(404).send('not found')
295+
})
296+
fastify.get('/echo', { websocket: true }, (conn, request) => {
297+
t.fail()
298+
})
299+
300+
fastify.listen(0, err => {
301+
t.error(err)
302+
303+
const port = fastify.server.address().port
304+
305+
const httpClient = net.createConnection({ port: port }, () => {
306+
t.teardown(httpClient.destroy.bind(httpClient))
307+
httpClient.write('GET /echo HTTP/1.1\r\n\r\n')
308+
httpClient.once('data', data => {
309+
t.match(data.toString(), /not found/i)
310+
})
311+
})
312+
})
313+
})
314+
315+
test('Should not hijack reply for an WS request to a WS route that gets sent a normal HTTP response in a hook', t => {
316+
t.plan(2)
317+
const fastify = Fastify()
318+
t.teardown(() => fastify.close())
319+
320+
fastify.register(fastifyWebsocket)
321+
fastify.addHook('preValidation', async (request, reply) => {
322+
await Promise.resolve()
323+
await reply.code(404).send('not found')
324+
})
325+
fastify.get('/echo', { websocket: true }, (conn, request) => {
326+
t.fail()
327+
})
328+
329+
fastify.listen(0, err => {
330+
t.error(err)
331+
332+
const ws = new WebSocket('ws://localhost:' + (fastify.server.address()).port + '/echo')
333+
const client = WebSocket.createWebSocketStream(ws, { encoding: 'utf8' })
334+
t.teardown(client.destroy.bind(client))
335+
336+
client.on('error', error => t.ok(error))
337+
})
338+
})

0 commit comments

Comments
 (0)