Skip to content

Commit 450adf1

Browse files
committed
feat: allow overriding or modifying the queryString for websockets
Allow overriding or modifying the queryString when proxying websockets. Fixes: #348
1 parent 8a48934 commit 450adf1

File tree

4 files changed

+222
-2
lines changed

4 files changed

+222
-2
lines changed

index.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,9 @@ class WebSocketProxy {
175175

176176
handleConnection (source, request, dest) {
177177
const url = this.findUpstream(request, dest)
178+
const queryString = getQueryString(url.search, request.url, this.wsClientOptions, request)
179+
url.search = queryString
180+
178181
const rewriteRequestHeaders = this.wsClientOptions.rewriteRequestHeaders
179182
const headersToRewrite = this.wsClientOptions.headers
180183

@@ -192,6 +195,22 @@ class WebSocketProxy {
192195
}
193196
}
194197

198+
function getQueryString (search, reqUrl, opts, request) {
199+
if (typeof opts.queryString === 'function') {
200+
return '?' + opts.queryString(search, reqUrl, request)
201+
}
202+
203+
if (opts.queryString) {
204+
return '?' + qs.stringify(opts.queryString)
205+
}
206+
207+
if (search.length > 0) {
208+
return search
209+
}
210+
211+
return ''
212+
}
213+
195214
function defaultWsHeadersRewrite (headers, request) {
196215
if (request.headers.cookie) {
197216
return { ...headers, cookie: request.headers.cookie }

test/websocket-querystring.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
'use strict'
2+
3+
const { test } = require('tap')
4+
const Fastify = require('fastify')
5+
const proxy = require('../')
6+
const WebSocket = require('ws')
7+
const { createServer } = require('node:http')
8+
const { promisify } = require('node:util')
9+
const { once } = require('node:events')
10+
const qs = require('fast-querystring')
11+
12+
const subprotocolValue = 'foo-subprotocol'
13+
14+
test('websocket proxy with object queryString', async (t) => {
15+
t.plan(7)
16+
17+
const origin = createServer()
18+
const wss = new WebSocket.Server({ server: origin })
19+
t.teardown(wss.close.bind(wss))
20+
t.teardown(origin.close.bind(origin))
21+
22+
const serverMessages = []
23+
wss.on('connection', (ws, request) => {
24+
t.equal(ws.protocol, subprotocolValue)
25+
t.equal(request.url, '/?q=test')
26+
ws.on('message', (message, binary) => {
27+
serverMessages.push([message.toString(), binary])
28+
// echo
29+
ws.send(message, { binary })
30+
})
31+
})
32+
33+
await promisify(origin.listen.bind(origin))({ port: 0, host: '127.0.0.1' })
34+
35+
const server = Fastify()
36+
server.register(proxy, {
37+
upstream: `ws://127.0.0.1:${origin.address().port}`,
38+
websocket: true,
39+
wsClientOptions: {
40+
queryString: { q: 'test' }
41+
}
42+
})
43+
44+
await server.listen({ port: 0, host: '127.0.0.1' })
45+
t.teardown(server.close.bind(server))
46+
47+
const ws = new WebSocket(`ws://127.0.0.1:${server.server.address().port}`, [subprotocolValue])
48+
await once(ws, 'open')
49+
50+
ws.send('hello', { binary: false })
51+
const [reply0, binary0] = await once(ws, 'message')
52+
t.equal(reply0.toString(), 'hello')
53+
t.equal(binary0, false)
54+
55+
ws.send(Buffer.from('fastify'), { binary: true })
56+
const [reply1, binary1] = await once(ws, 'message')
57+
t.equal(reply1.toString(), 'fastify')
58+
t.equal(binary1, true)
59+
60+
t.strictSame(serverMessages, [
61+
['hello', false],
62+
['fastify', true]
63+
])
64+
65+
await Promise.all([
66+
once(ws, 'close'),
67+
server.close()
68+
])
69+
})
70+
71+
test('websocket proxy with function queryString', async (t) => {
72+
t.plan(7)
73+
74+
const origin = createServer()
75+
const wss = new WebSocket.Server({ server: origin })
76+
t.teardown(wss.close.bind(wss))
77+
t.teardown(origin.close.bind(origin))
78+
79+
const serverMessages = []
80+
wss.on('connection', (ws, request) => {
81+
t.equal(ws.protocol, subprotocolValue)
82+
t.equal(request.url, '/?q=test')
83+
ws.on('message', (message, binary) => {
84+
serverMessages.push([message.toString(), binary])
85+
// echo
86+
ws.send(message, { binary })
87+
})
88+
})
89+
90+
await promisify(origin.listen.bind(origin))({ port: 0, host: '127.0.0.1' })
91+
92+
const server = Fastify()
93+
server.register(proxy, {
94+
upstream: `ws://127.0.0.1:${origin.address().port}`,
95+
websocket: true,
96+
wsClientOptions: {
97+
queryString: () => qs.stringify({ q: 'test' })
98+
}
99+
})
100+
101+
await server.listen({ port: 0, host: '127.0.0.1' })
102+
t.teardown(server.close.bind(server))
103+
104+
const ws = new WebSocket(`ws://127.0.0.1:${server.server.address().port}`, [subprotocolValue])
105+
await once(ws, 'open')
106+
107+
ws.send('hello', { binary: false })
108+
const [reply0, binary0] = await once(ws, 'message')
109+
t.equal(reply0.toString(), 'hello')
110+
t.equal(binary0, false)
111+
112+
ws.send(Buffer.from('fastify'), { binary: true })
113+
const [reply1, binary1] = await once(ws, 'message')
114+
t.equal(reply1.toString(), 'fastify')
115+
t.equal(binary1, true)
116+
117+
t.strictSame(serverMessages, [
118+
['hello', false],
119+
['fastify', true]
120+
])
121+
122+
await Promise.all([
123+
once(ws, 'close'),
124+
server.close()
125+
])
126+
})

test/websocket.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,68 @@ test('websocket proxy with rewriteRequestHeaders', async (t) => {
315315
])
316316
})
317317

318+
test('websocket proxy with queryString', async (t) => {
319+
t.plan(7)
320+
321+
const origin = createServer()
322+
const wss = new WebSocket.Server({ server: origin })
323+
t.teardown(wss.close.bind(wss))
324+
t.teardown(origin.close.bind(origin))
325+
326+
const serverMessages = []
327+
wss.on('connection', (ws, request) => {
328+
t.equal(ws.protocol, subprotocolValue)
329+
t.equal(request.headers.myauth, 'myauth')
330+
ws.on('message', (message, binary) => {
331+
serverMessages.push([message.toString(), binary])
332+
// echo
333+
ws.send(message, { binary })
334+
})
335+
})
336+
337+
await promisify(origin.listen.bind(origin))({ port: 0, host: '127.0.0.1' })
338+
339+
const server = Fastify()
340+
server.register(proxy, {
341+
upstream: `ws://127.0.0.1:${origin.address().port}`,
342+
websocket: true,
343+
wsClientOptions: {
344+
rewriteRequestHeaders: (headers, request) => {
345+
return {
346+
...headers,
347+
myauth: 'myauth'
348+
}
349+
}
350+
}
351+
})
352+
353+
await server.listen({ port: 0, host: '127.0.0.1' })
354+
t.teardown(server.close.bind(server))
355+
356+
const ws = new WebSocket(`ws://127.0.0.1:${server.server.address().port}`, [subprotocolValue])
357+
await once(ws, 'open')
358+
359+
ws.send('hello', { binary: false })
360+
const [reply0, binary0] = await once(ws, 'message')
361+
t.equal(reply0.toString(), 'hello')
362+
t.equal(binary0, false)
363+
364+
ws.send(Buffer.from('fastify'), { binary: true })
365+
const [reply1, binary1] = await once(ws, 'message')
366+
t.equal(reply1.toString(), 'fastify')
367+
t.equal(binary1, true)
368+
369+
t.strictSame(serverMessages, [
370+
['hello', false],
371+
['fastify', true]
372+
])
373+
374+
await Promise.all([
375+
once(ws, 'close'),
376+
server.close()
377+
])
378+
})
379+
318380
test('websocket proxy custom headers', async (t) => {
319381
t.plan(7)
320382

types/index.d.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
/// <reference types='node' />
22

3-
import { FastifyPluginCallback, preHandlerHookHandler, preValidationHookHandler } from 'fastify';
3+
import {
4+
FastifyPluginCallback,
5+
FastifyRequest,
6+
preHandlerHookHandler,
7+
preValidationHookHandler,
8+
RawServerBase,
9+
RequestGenericInterface,
10+
} from 'fastify';
411

512
import {
613
FastifyReplyFromOptions,
@@ -24,6 +31,12 @@ type FastifyHttpProxy = FastifyPluginCallback<
2431
>;
2532

2633
declare namespace fastifyHttpProxy {
34+
type QueryStringFunction = (
35+
search: string | undefined,
36+
reqUrl: string,
37+
request: FastifyRequest<RequestGenericInterface, RawServerBase>
38+
) => string;
39+
2740
export interface FastifyHttpProxyOptions extends FastifyReplyFromOptions {
2841
upstream: string;
2942
prefix?: string;
@@ -34,7 +47,7 @@ declare namespace fastifyHttpProxy {
3447
preValidation?: preValidationHookHandler;
3548
config?: Object;
3649
replyOptions?: FastifyReplyFromHooks;
37-
wsClientOptions?: ClientOptions;
50+
wsClientOptions?: ClientOptions & { queryString?: { [key: string]: unknown } | QueryStringFunction; };
3851
wsServerOptions?: ServerOptions;
3952
httpMethods?: string[];
4053
constraints?: { [name: string]: any };

0 commit comments

Comments
 (0)