@fastify/websocket
Advanced tools
Comparing version 8.3.1 to 9.0.0
60
index.js
'use strict' | ||
const { ServerResponse } = require('node:http') | ||
const { PassThrough } = require('node:stream') | ||
const { randomBytes } = require('node:crypto') | ||
const fp = require('fastify-plugin') | ||
const WebSocket = require('ws') | ||
const Duplexify = require('duplexify') | ||
@@ -50,2 +53,56 @@ const kWs = Symbol('ws-socket') | ||
async function injectWS (path = '/', upgradeContext = {}) { | ||
const server2Client = new PassThrough() | ||
const client2Server = new PassThrough() | ||
const serverStream = new Duplexify(server2Client, client2Server) | ||
const clientStream = new Duplexify(client2Server, server2Client) | ||
const ws = new WebSocket(null, undefined, { isServer: false }) | ||
const head = Buffer.from([]) | ||
let resolve, reject | ||
const promise = new Promise((_resolve, _reject) => { resolve = _resolve; reject = _reject }) | ||
ws.on('open', () => { | ||
clientStream.removeListener('data', onData) | ||
resolve(ws) | ||
}) | ||
const onData = (chunk) => { | ||
if (chunk.toString().includes('HTTP/1.1 101 Switching Protocols')) { | ||
ws._isServer = false | ||
ws.setSocket(clientStream, head, { maxPayload: 0 }) | ||
} else { | ||
clientStream.removeListener('data', onData) | ||
const statusCode = Number(chunk.toString().match(/HTTP\/1.1 (\d+)/)[1]) | ||
reject(new Error('Unexpected server response: ' + statusCode)) | ||
} | ||
} | ||
clientStream.on('data', onData) | ||
const req = { | ||
...upgradeContext, | ||
method: 'GET', | ||
headers: { | ||
...upgradeContext.headers, | ||
connection: 'upgrade', | ||
upgrade: 'websocket', | ||
'sec-websocket-version': 13, | ||
'sec-websocket-key': randomBytes(16).toString('base64') | ||
}, | ||
httpVersion: '1.1', | ||
url: path, | ||
[kWs]: serverStream, | ||
[kWsHead]: head | ||
} | ||
websocketListenServer.emit('upgrade', req, req[kWs], req[kWsHead]) | ||
return promise | ||
} | ||
fastify.decorate('injectWS', injectWS) | ||
function onUpgrade (rawRequest, socket, head) { | ||
@@ -168,2 +225,3 @@ // Save a reference to the socket and then dispatch the request through the normal fastify router so that it will invoke hooks and then eventually a route handler that might upgrade the socket. | ||
} | ||
fastify.server.removeListener('upgrade', onUpgrade) | ||
@@ -186,3 +244,3 @@ | ||
// Reference: https://github.com/websockets/ws/blob/master/lib/stream.js#L35 | ||
conn.on('error', _ => {}) | ||
conn.on('error', _ => { }) | ||
request.log.error(error) | ||
@@ -189,0 +247,0 @@ conn.destroy(error) |
{ | ||
"name": "@fastify/websocket", | ||
"version": "8.3.1", | ||
"version": "9.0.0", | ||
"description": "basic websocket support for fastify", | ||
@@ -30,3 +30,3 @@ "main": "index.js", | ||
"@fastify/pre-commit": "^2.0.2", | ||
"@fastify/type-provider-typebox": "^3.2.0", | ||
"@fastify/type-provider-typebox": "^4.0.0", | ||
"@types/node": "^20.1.0", | ||
@@ -39,5 +39,6 @@ "@types/ws": "^8.2.2", | ||
"tap": "^16.0.0", | ||
"tsd": "^0.29.0" | ||
"tsd": "^0.30.1" | ||
}, | ||
"dependencies": { | ||
"duplexify": "^4.1.2", | ||
"fastify-plugin": "^4.0.0", | ||
@@ -44,0 +45,0 @@ "ws": "^8.0.0" |
@@ -259,2 +259,74 @@ # @fastify/websocket | ||
### Testing | ||
Testing the ws handler can be quite tricky, luckily `fastify-websocket` decorates fastify instance with `injectWS`. | ||
It allows to test easily a websocket endpoint. | ||
The signature of injectWS is the following: `([path], [upgradeContext])`. | ||
#### App.js | ||
```js | ||
'use strict' | ||
const Fastify = require('fastify') | ||
const FastifyWebSocket = require('@fastify/websocket') | ||
const App = Fastify() | ||
App.register(FastifyWebSocket); | ||
App.register(async function(fastify) { | ||
fastify.addHook('preValidation', async (request, reply) => { | ||
if (request.headers['api-key'] !== 'some-random-key') { | ||
return reply.code(401).send() | ||
} | ||
}) | ||
fastify.get('/', { websocket: true }, (connection) => { | ||
connection.socket.on('message', message => { | ||
connection.socket.send('hi from server') | ||
}) | ||
}) | ||
}) | ||
module.exports = App | ||
``` | ||
#### App.test.js | ||
```js | ||
'use strict' | ||
const { test } = require('tap') | ||
const Fastify = require('fastify') | ||
const App = require('./app.js') | ||
test('connect to /', async (t) => { | ||
t.plan(1) | ||
const fastify = Fastify() | ||
fastify.register(App) | ||
t.teardown(fastify.close.bind(fastify)) | ||
const ws = await fastify.injectWS('/', {headers: { "api-key" : "some-random-key" }}) | ||
let resolve; | ||
const promise = new Promise(r => { resolve = r }) | ||
ws.on('message', (data) => { | ||
resolve(data.toString()); | ||
}) | ||
ws.send('hi from client') | ||
t.assert(await promise, 'hi from server') | ||
// Remember to close the ws at the end | ||
ws.terminate() | ||
}) | ||
``` | ||
#### Things to know | ||
- Websocket need to be closed manually at the end of each test. | ||
- `fastify.ready()` needs to be awaited to ensure that fastify has been decorated. | ||
- You need to register the event listener before sending the message if you need to process server response. | ||
## Options | ||
@@ -261,0 +333,0 @@ |
@@ -30,5 +30,8 @@ /// <reference types="node" /> | ||
type InjectWSFn<RawRequest> = | ||
((path?: string, upgradeContext?: Partial<RawRequest>) => Promise<WebSocket>) | ||
interface FastifyInstance<RawServer, RawRequest, RawReply, Logger, TypeProvider> { | ||
get: RouteShorthandMethod<RawServer, RawRequest, RawReply, TypeProvider, Logger>, | ||
websocketServer: WebSocket.Server, | ||
injectWS: InjectWSFn<RawRequest> | ||
} | ||
@@ -71,3 +74,2 @@ | ||
interface WebSocketServerOptions extends Omit<WebSocket.ServerOptions, "path"> { } | ||
export type WebsocketHandler< | ||
@@ -86,7 +88,5 @@ RawServer extends RawServerBase = RawServerDefault, | ||
) => void | Promise<any>; | ||
export interface SocketStream extends Duplex { | ||
socket: WebSocket; | ||
} | ||
export interface WebsocketPluginOptions { | ||
@@ -98,3 +98,2 @@ errorHandler?: (this: FastifyInstance, error: Error, connection: SocketStream, request: FastifyRequest, reply: FastifyReply) => void; | ||
} | ||
export interface RouteOptions< | ||
@@ -101,0 +100,0 @@ RawServer extends RawServerBase = RawServerDefault, |
90536
16
2033
370
3
+ Addedduplexify@^4.1.2
+ Addedduplexify@4.1.3(transitive)
+ Addedend-of-stream@1.4.4(transitive)
+ Addedinherits@2.0.4(transitive)
+ Addedonce@1.4.0(transitive)
+ Addedreadable-stream@3.6.2(transitive)
+ Addedsafe-buffer@5.2.1(transitive)
+ Addedstream-shift@1.0.3(transitive)
+ Addedstring_decoder@1.3.0(transitive)
+ Addedutil-deprecate@1.0.2(transitive)
+ Addedwrappy@1.0.2(transitive)