Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,8 @@ The following options are provided by the underlying [httpxy](https://github.com

## WebSocket

See [recipes/websocket.md](recipes/websocket.md) for more examples.

```javascript
// verbose api
createProxyMiddleware({ pathFilter: '/', target: 'http://echo.websocket.org', ws: true });
Expand Down
70 changes: 52 additions & 18 deletions recipes/websocket.md
Original file line number Diff line number Diff line change
@@ -1,50 +1,84 @@
# WebSocket

This example will create a proxy middleware with websocket support.
Examples to use `http-proxy-middleware` with WebSocket support.

- [WebSocket - `ws:true` flag (automatic upgrade subscription)](#websocket---wstrue-flag-automatic-upgrade-subscription)
- [WebSocket - Manual server upgrade subscription](#websocket---manual-server-upgrade-subscription)
- [Multiple WebSocket targets](#multiple-websocket-targets)
- [WebSocket - Path Rewrite](#websocket---path-rewrite)

## WebSocket - `ws:true` flag (automatic upgrade subscription)

⚠️ NOTE: Using `ws: true` requires an an initial regular HTTP request, so HPM can subscribe to server upgrade event internally.

💡 Use `server.on('upgrade', proxy.upgrade)` without the need of an initial HTTP request.

```javascript
import { createProxyMiddleware } from 'http-proxy-middleware';

const socketProxy = createProxyMiddleware({
target: 'http://localhost:3000',
pathFilter: '/socket',
ws: true,
});
```

## WebSocket - Path Rewrite
## WebSocket - Manual server upgrade subscription

This example will create a proxy middleware with websocket support and pathRewrite.
Manually subscribe to server's upgrade event.

```javascript
import { createProxyMiddleware } from 'http-proxy-middleware';

const options = {
const socketProxy = createProxyMiddleware({
target: 'http://localhost:3000',
ws: true,
pathFilter: '/socket',
pathRewrite: {
'^/socket': '',
},
};
ws: true,
});

const socketProxy = createProxyMiddleware(options);
server.on('upgrade', socketProxy.upgrade); // <-- subscribe to http 'upgrade'
```

## WebSocket - Server update subscription
## Multiple WebSocket targets

Mount each websocket proxy with different target on its own route.

```javascript
import express from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware';

const app = express();

const wsProxyA = createProxyMiddleware({
target: 'http://localhost:4001',
pathFilter: '/ws-path-a',
ws: true,
});

This example will create a proxy middleware with websocket support.
const wsProxyB = createProxyMiddleware({
target: 'http://localhost:4002',
pathFilter: '/ws-path-b',
ws: true,
});

Subscribe to server's upgrade event.
app.use('/ws-path-a', wsProxyA);
app.use('/ws-path-b', wsProxyB);
```

## WebSocket - Path Rewrite

This example will create a proxy middleware with websocket support and pathRewrite.

```javascript
import { createProxyMiddleware } from 'http-proxy-middleware';

const socketProxy = createProxyMiddleware({
const options = {
target: 'http://localhost:3000',
pathFilter: '/socket',
ws: true,
});
pathFilter: '/socket',
pathRewrite: {
'^/socket': '',
},
};

server.on('upgrade', socketProxy.upgrade); // <-- subscribe to http 'upgrade'
const socketProxy = createProxyMiddleware(options);
```
115 changes: 114 additions & 1 deletion test/e2e/websocket.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { WebSocket } from 'ws';

import type { RequestHandler } from '../../src/types.js';
import { createMockRequest, createMockResponse } from '../test-utils.js';
import { createApp, createProxyMiddleware } from './test-kit.js';
import { createApp, createAppWithPath, createProxyMiddleware } from './test-kit.js';

/********************************************************************
* - Not possible to use `supertest` to test WebSockets
Expand Down Expand Up @@ -123,6 +123,119 @@ describe('E2E WebSocket proxy', () => {

await expect(messageReceived).resolves.toBe('foobar');
});

it('should forward websocket upgrade to /ws-path without duplicating the mounted path', async () => {
const sockJsPort = await getPort();

const sockJsTargetServer = getLocal();
await sockJsTargetServer.start();
// performs an initial regular HTTP GET request subscribe to server upgrade event internally.
await sockJsTargetServer.forGet('/ws-path').thenReply(200, 'ok');
const sockJsRule = await sockJsTargetServer.forAnyWebSocket().thenEcho();

const sockJsMiddleware = createProxyMiddleware({
target: `${sockJsTargetServer.url}`,
ws: true,
});

const sockJsServer = createAppWithPath('/ws-path', sockJsMiddleware).listen(sockJsPort);

await new Promise((resolve, reject) => {
http.get(`http://localhost:${sockJsPort}/ws-path`, resolve).on('error', reject);
});

const sockJsClient = new WebSocket(`ws://localhost:${sockJsPort}/ws-path`);

try {
await new Promise<void>((resolve, reject) => {
sockJsClient.once('open', () => resolve());
sockJsClient.once('error', reject);
});

const seenRequests = await sockJsRule.getSeenRequests();
expect(seenRequests).toHaveLength(1);
const upstreamPathname = new URL(seenRequests[0].url).pathname;
expect(upstreamPathname).toBe('/ws-path');
} finally {
await Promise.all([
closeWebSocketClient(sockJsClient),
closeServer(sockJsServer),
sockJsTargetServer.stop(),
]);
}
});

it('should proxy websocket upgrades for two mounted paths with separate targets', async () => {
const serverPort = await getPort();

const targetServerA = getLocal();
const targetServerB = getLocal();
await targetServerA.start();
await targetServerB.start();
await targetServerA.forGet('/ws-path-a').thenReply(200, 'ok');
await targetServerB.forGet('/ws-path-b').thenReply(200, 'ok');
await targetServerA.forAnyWebSocket().thenEcho();
await targetServerB.forAnyWebSocket().thenEcho();

const middlewareA = createProxyMiddleware({
target: targetServerA.url,
ws: true,
// NOTE: when multiple ws middlewares share one server, scope each with pathFilter
// so only the intended middleware handles a given upgrade request.
pathFilter: '/ws-path-a',
});

const middlewareB = createProxyMiddleware({
target: targetServerB.url,
ws: true,
pathFilter: '/ws-path-b',
});

const multiMountServer = createAppWithPath('/ws-path-a', middlewareA)
.use('/ws-path-b', middlewareB)
.listen(serverPort);

const connectAndEcho = async (path: string, message: string) => {
const socket = new WebSocket(`ws://localhost:${serverPort}${path}`);

try {
await new Promise<void>((resolve, reject) => {
socket.once('open', () => resolve());
socket.once('error', reject);
});

const echoed = new Promise<string>((resolve) => {
socket.once('message', (data) => resolve(data.toString()));
});

socket.send(message);
await expect(echoed).resolves.toBe(message);
} finally {
await closeWebSocketClient(socket);
}
};

try {
// NOTE: perform a normal HTTP request per mounted path first so each middleware
// subscribes to the server upgrade event before the websocket connection starts.
await new Promise((resolve, reject) => {
http.get(`http://localhost:${serverPort}/ws-path-a`, resolve).on('error', reject);
});

await new Promise((resolve, reject) => {
http.get(`http://localhost:${serverPort}/ws-path-b`, resolve).on('error', reject);
});

await connectAndEcho('/ws-path-a', 'from-a');
await connectAndEcho('/ws-path-b', 'from-b');
} finally {
await Promise.all([
closeServer(multiMountServer),
targetServerA.stop(),
targetServerB.stop(),
]);
}
});
});

describe('option.ws with external server "upgrade"', () => {
Expand Down
Loading