'use strict' const net = require('node:net') const test = require('tap').test const Fastify = require('fastify') const fastifyWebsocket = require('..') const WebSocket = require('ws') const get = require('node:http').get test('Should expose a websocket on prefixed route', t => { t.plan(4) const fastify = Fastify() t.teardown(() => fastify.close()) fastify.register(fastifyWebsocket) fastify.register( function (instance, _opts, next) { instance.get('/echo', { websocket: true }, function (socket) { t.equal(this.prefix, '/baz') socket.send('hello client') t.teardown(() => socket.terminate()) socket.once('message', (chunk) => { t.equal(chunk.toString(), 'hello server') }) }) next() }, { prefix: '/baz' } ) fastify.listen({ port: 0 }, err => { t.error(err) const ws = new WebSocket('ws://localhost:' + (fastify.server.address()).port + '/baz/echo') const client = WebSocket.createWebSocketStream(ws, { encoding: 'utf8' }) t.teardown(client.destroy.bind(client)) client.setEncoding('utf8') client.write('hello server') client.once('data', chunk => { t.equal(chunk, 'hello client') client.end() }) }) }) test('Should expose a websocket on prefixed route with /', t => { t.plan(3) const fastify = Fastify() t.teardown(() => fastify.close()) fastify.register(fastifyWebsocket) fastify.register( function (instance, _opts, next) { instance.get('/', { websocket: true }, (socket) => { socket.send('hello client') t.teardown(() => socket.terminate()) socket.once('message', (chunk) => { t.equal(chunk.toString(), 'hello server') }) }) next() }, { prefix: '/baz' } ) fastify.listen({ port: 0 }, err => { t.error(err) const ws = new WebSocket('ws://localhost:' + (fastify.server.address()).port + '/baz') const client = WebSocket.createWebSocketStream(ws, { encoding: 'utf8' }) t.teardown(client.destroy.bind(client)) client.setEncoding('utf8') client.write('hello server') client.once('data', chunk => { t.equal(chunk, 'hello client') client.end() }) }) }) test('Should expose websocket and http route', t => { t.plan(4) const fastify = Fastify() t.teardown(() => fastify.close()) fastify.register(fastifyWebsocket) fastify.register( function (instance, _opts, next) { instance.route({ method: 'GET', url: '/echo', handler: (_request, reply) => { reply.send({ hello: 'world' }) }, wsHandler: (socket) => { socket.send('hello client') t.teardown(() => socket.terminate()) socket.once('message', (chunk) => { t.equal(chunk.toString(), 'hello server') }) } }) next() }, { prefix: '/baz' } ) fastify.listen({ port: 0 }, err => { t.error(err) const url = '//localhost:' + (fastify.server.address()).port + '/baz/echo' const ws = new WebSocket('ws:' + url) const client = WebSocket.createWebSocketStream(ws, { encoding: 'utf8' }) t.teardown(client.destroy.bind(client)) client.setEncoding('utf8') client.write('hello server') client.once('data', chunk => { t.equal(chunk, 'hello client') client.end() }) get('http:' + url, function (response) { let data = '' // A chunk of data has been recieved. response.on('data', (chunk) => { data += chunk }) // The whole response has been received. Print out the result. response.on('end', () => { t.equal(data, '{"hello":"world"}') }) }) }) }) test('Should close on unregistered path (with no wildcard route websocket handler defined)', t => { t.plan(2) const fastify = Fastify() t.teardown(() => fastify.close()) fastify .register(fastifyWebsocket) .register(async function () { fastify.get('/*', (_request, reply) => { reply.send('hello world') }) fastify.get('/echo', { websocket: true }, (socket) => { socket.on('message', message => { try { socket.send(message) } catch (err) { socket.send(err.message) } }) t.teardown(() => socket.terminate()) }) }) fastify.listen({ port: 0 }, err => { t.error(err) const ws = new WebSocket('ws://localhost:' + (fastify.server.address()).port) const client = WebSocket.createWebSocketStream(ws, { encoding: 'utf8' }) t.teardown(client.destroy.bind(client)) ws.on('close', () => { t.pass() }) }) }) test('Should use wildcard websocket route when (with a normal http wildcard route defined as well)', t => { t.plan(2) const fastify = Fastify() t.teardown(() => fastify.close()) fastify .register(fastifyWebsocket) .register(async function (fastify) { fastify.route({ method: 'GET', url: '/*', handler: (_, reply) => { reply.send({ hello: 'world' }) }, wsHandler: (socket) => { socket.send('hello client') t.teardown(() => socket.terminate()) socket.once('message', () => { socket.close() }) } }) }) fastify.listen({ port: 0 }, err => { t.error(err) const ws = new WebSocket('ws://localhost:' + (fastify.server.address()).port) const client = WebSocket.createWebSocketStream(ws, { encoding: 'utf8' }) t.teardown(client.destroy.bind(client)) client.once('data', chunk => { t.equal(chunk, 'hello client') client.end() }) }) }) test('Should call wildcard route handler on unregistered path', t => { t.plan(3) const fastify = Fastify() t.teardown(() => fastify.close()) fastify .register(fastifyWebsocket) .register(async function (fastify) { fastify.get('/*', { websocket: true }, (socket) => { socket.on('message', () => { try { socket.send('hi from wildcard route handler') } catch (err) { socket.send(err.message) } }) t.teardown(() => socket.terminate()) }) }) fastify.get('/echo', { websocket: true }, (socket) => { socket.on('message', () => { try { socket.send('hi from /echo handler') } catch (err) { socket.send(err.message) } }) t.teardown(() => socket.terminate()) }) fastify.listen({ port: 0 }, err => { t.error(err) const ws = new WebSocket('ws://localhost:' + (fastify.server.address()).port) const client = WebSocket.createWebSocketStream(ws, { encoding: 'utf8' }) t.teardown(client.destroy.bind(client)) ws.on('open', () => { ws.send('hi from client') client.end() }) ws.on('message', message => { t.equal(message.toString(), 'hi from wildcard route handler') }) ws.on('close', () => { t.pass() }) }) }) test('Should invoke the correct handler depending on the headers', t => { t.plan(4) const fastify = Fastify() t.teardown(() => fastify.close()) fastify.register(fastifyWebsocket) fastify.register(async function () { fastify.route({ method: 'GET', url: '/', handler: (_request, reply) => { reply.send('hi from handler') }, wsHandler: (socket) => { socket.send('hi from wsHandler') t.teardown(() => socket.terminate()) } }) }) fastify.listen({ port: 0 }, err => { t.error(err) const port = fastify.server.address().port const httpClient = net.createConnection({ port }, () => { httpClient.write('GET / HTTP/1.1\r\nHOST: localhost\r\n\r\n') httpClient.once('data', data => { t.match(data.toString(), /hi from handler/i) httpClient.end() }) }) const wsClient = net.createConnection({ port }, () => { wsClient.write('GET / HTTP/1.1\r\nConnection: upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n') wsClient.once('data', data => { t.match(data.toString(), /hi from wsHandler/i) wsClient.end(() => { t.pass() }) }) }) }) }) test('Should call the wildcard handler if a no other non-websocket route with path exists', t => { const fastify = Fastify() t.teardown(() => fastify.close()) fastify.register(fastifyWebsocket) fastify.register(async function (fastify) { fastify.get('/*', { websocket: true }, (socket) => { t.ok('called', 'wildcard handler') socket.close() t.teardown(() => socket.terminate()) }) fastify.get('/http', (_request, reply) => { t.fail('Should not call http handler') reply.send('http route') }) }) fastify.listen({ port: 0 }, err => { t.error(err) const ws = new WebSocket('ws://localhost:' + (fastify.server.address()).port + '/http2') const client = WebSocket.createWebSocketStream(ws, { encoding: 'utf8' }) t.teardown(client.destroy.bind(client)) client.setEncoding('utf8') client.end(() => { t.end() }) }) }) test('Should close the connection if a non-websocket route with path exists', t => { t.plan(2) const fastify = Fastify() t.teardown(() => fastify.close()) fastify.register(fastifyWebsocket) fastify.register(async function (fastify) { fastify.get('/*', { websocket: true }, (socket) => { t.fail('called', 'wildcard handler') t.teardown(() => socket.terminate()) }) fastify.get('/http', (_request, reply) => { t.fail('Should not call /http handler') reply.send('http route') }) }) fastify.listen({ port: 0 }, err => { t.error(err) const ws = new WebSocket('ws://localhost:' + (fastify.server.address()).port + '/http') ws.on('close', (code) => { t.equal(code, 1005, 'closed websocket') t.end() }) }) }) test('Should throw on wrong HTTP method', t => { t.plan(2) const fastify = Fastify() t.teardown(() => fastify.close()) fastify.register(fastifyWebsocket) fastify.register(async function (fastify) { fastify.post('/echo', { websocket: true }, (socket) => { socket.on('message', message => { try { socket.send(message) } catch (err) { socket.send(err.message) } }) t.teardown(() => socket.terminate()) }) fastify.get('/http', (_request, reply) => { t.fail('Should not call /http handler') reply.send('http route') }) }) fastify.listen({ port: 0 }, (err) => { t.ok(err) t.equal(err.message, 'websocket handler can only be declared in GET method') }) }) test('Should throw on invalid wsHandler', async t => { t.plan(1) const fastify = Fastify() t.teardown(() => fastify.close()) await fastify.register(fastifyWebsocket) try { fastify.route({ method: 'GET', url: '/echo', handler: (_, reply) => { reply.send({ hello: 'world' }) }, wsHandler: 'hello' }, { prefix: '/baz' }) } catch (err) { t.equal(err.message, 'invalid wsHandler function') } }) test('Should open on registered path', t => { t.plan(2) const fastify = Fastify() t.teardown(() => fastify.close()) fastify.register(fastifyWebsocket) fastify.register(async function (fastify) { fastify.get('/echo', { websocket: true }, (socket) => { socket.on('message', message => { try { socket.send(message) } catch (err) { socket.send(err.message) } }) t.teardown(() => socket.terminate()) }) }) fastify.listen({ port: 0 }, err => { t.error(err) const ws = new WebSocket('ws://localhost:' + (fastify.server.address()).port + '/echo') ws.on('open', () => { t.pass() client.end() }) const client = WebSocket.createWebSocketStream(ws, { encoding: 'utf8' }) t.teardown(client.destroy.bind(client)) }) }) test('Should send message and close', t => { t.plan(5) const fastify = Fastify() t.teardown(() => fastify.close()) fastify.register(fastifyWebsocket) fastify.register(async function (fastify) { fastify.get('/', { websocket: true }, (socket) => { socket.on('message', message => { t.equal(message.toString(), 'hi from client') socket.send('hi from server') }) socket.on('close', () => { t.pass() }) t.teardown(() => socket.terminate()) }) }) fastify.listen({ port: 0 }, err => { t.error(err) const ws = new WebSocket('ws://localhost:' + (fastify.server.address()).port + '/') const client = WebSocket.createWebSocketStream(ws, { encoding: 'utf8' }) t.teardown(client.destroy.bind(client)) ws.on('message', message => { t.equal(message.toString(), 'hi from server') }) ws.on('open', () => { ws.send('hi from client') client.end() }) ws.on('close', () => { t.pass() }) }) }) test('Should return 404 on http request', t => { const fastify = Fastify() t.teardown(() => fastify.close()) fastify.register(fastifyWebsocket) fastify.register(async function (fastify) { fastify.get('/', { websocket: true }, (socket) => { socket.on('message', message => { t.equal(message.toString(), 'hi from client') socket.send('hi from server') }) socket.on('close', () => { t.pass() }) t.teardown(() => socket.terminate()) }) }) fastify.inject({ method: 'GET', url: '/' }).then((response) => { t.equal(response.payload, '') t.equal(response.statusCode, 404) t.end() }) }) test('Should pass route params to per-route handlers', t => { const fastify = Fastify() t.teardown(() => fastify.close()) fastify.register(fastifyWebsocket) fastify.register(async function (fastify) { fastify.get('/ws', { websocket: true }, (socket, request) => { const params = request.params t.equal(Object.keys(params).length, 0, 'params are empty') socket.send('empty') socket.close() }) fastify.get('/ws/:id', { websocket: true }, (socket, request) => { const params = request.params t.equal(params.id, 'foo', 'params are correct') socket.send(params.id) socket.close() }) }) fastify.listen({ port: 0 }, err => { let pending = 2 t.error(err) const ws = new WebSocket( 'ws://localhost:' + (fastify.server.address()).port + '/ws/foo' ) const client = WebSocket.createWebSocketStream(ws, { encoding: 'utf8' }) const ws2 = new WebSocket( 'ws://localhost:' + (fastify.server.address()).port + '/ws' ) const client2 = WebSocket.createWebSocketStream(ws2, { encoding: 'utf8' }) t.teardown(client.destroy.bind(client)) t.teardown(client2.destroy.bind(client2)) client.setEncoding('utf8') client2.setEncoding('utf8') client.once('data', chunk => { t.equal(chunk, 'foo') client.end() if (--pending === 0) t.end() }) client2.once('data', chunk => { t.equal(chunk, 'empty') client2.end() if (--pending === 0) t.end() }) }) }) test('Should not throw error when register empty get with prefix', t => { const fastify = Fastify() t.teardown(() => fastify.close()) fastify.register(fastifyWebsocket) fastify.register( function (instance, _opts, next) { instance.get('/', { websocket: true }, (socket) => { socket.on('message', message => { t.equal(message.toString(), 'hi from client') socket.send('hi from server') }) }) next() }, { prefix: '/baz' } ) fastify.listen({ port: 0 }, err => { if (err) t.error(err) const ws = new WebSocket( 'ws://localhost:' + fastify.server.address().port + '/baz/' ) ws.on('open', () => { t.pass('Done') ws.close() t.end() }) }) }) test('Should expose fastify instance to websocket per-route handler', t => { const fastify = Fastify() t.teardown(() => fastify.close()) fastify.register(fastifyWebsocket) fastify.register(async function (fastify) { fastify.get('/ws', { websocket: true }, function wsHandler (socket) { t.equal(this, fastify, 'this is bound to fastify server') socket.send('empty') socket.close() }) }) fastify.listen({ port: 0 }, err => { t.error(err) const ws = new WebSocket( 'ws://localhost:' + (fastify.server.address()).port + '/ws' ) const client = WebSocket.createWebSocketStream(ws, { encoding: 'utf8' }) t.teardown(client.destroy.bind(client)) client.setEncoding('utf8') client.once('data', chunk => { t.equal(chunk, 'empty') client.end() t.end() }) }) }) test('Should have access to decorators in per-route handler', t => { const fastify = Fastify() t.teardown(() => fastify.close()) fastify.decorateRequest('str', 'it works!') fastify.register(fastifyWebsocket) fastify.register(async function (fastify) { fastify.get('/ws', { websocket: true }, function wsHandler (socket, request) { t.equal(request.str, 'it works!', 'decorator is accessible') socket.send('empty') socket.close() }) }) fastify.listen({ port: 0 }, err => { t.error(err) const ws = new WebSocket('ws://localhost:' + (fastify.server.address()).port + '/ws') const client = WebSocket.createWebSocketStream(ws, { encoding: 'utf8' }) t.teardown(client.destroy.bind(client)) client.once('data', chunk => { t.equal(chunk, 'empty') client.end() t.end() }) }) }) test('should call `destroy` when exception is thrown inside async handler', t => { t.plan(2) const fastify = Fastify() t.teardown(() => fastify.close()) fastify.register(fastifyWebsocket) fastify.register(async function (fastify) { fastify.get('/ws', { websocket: true }, async function wsHandler (socket) { socket.on('close', code => { t.equal(code, 1006) t.end() }) throw new Error('something wrong') }) }) fastify.listen({ port: 0 }, err => { t.error(err) const ws = new WebSocket( 'ws://localhost:' + (fastify.server.address()).port + '/ws' ) const client = WebSocket.createWebSocketStream(ws, { encoding: 'utf8' }) client.on('error', (_) => { }) t.teardown(client.destroy.bind(client)) }) }) test('should call default non websocket fastify route when no match is found', t => { t.plan(2) const fastify = Fastify() t.teardown(() => fastify.close()) fastify.register(fastifyWebsocket) fastify.register(async function (fastify) { fastify.get('/ws', function handler (_request, reply) { reply.send({ hello: 'world' }) }) }) fastify.listen({ port: 0 }, err => { t.error(err) get('http://localhost:' + (fastify.server.address()).port + '/wrong-route', function (response) { t.equal(response.statusCode, 404) t.end() }) }) }) test('register a non websocket route', t => { t.plan(2) const fastify = Fastify() t.teardown(() => fastify.close()) fastify.register(fastifyWebsocket) fastify.register(async function (fastify) { fastify.get('/ws', function handler (_request, reply) { reply.send({ hello: 'world' }) }) }) fastify.listen({ port: 0 }, err => { t.error(err) get('http://localhost:' + (fastify.server.address()).port + '/ws', function (response) { let data = '' response.on('data', (chunk) => { data += chunk }) response.on('end', () => { t.equal(data, '{"hello":"world"}') t.end() }) }) }) })