From f2c708923f9f6a53d5e2254d45f6f129634a1fe3 Mon Sep 17 00:00:00 2001 From: Benjamin Newman Date: Thu, 12 Feb 2026 17:31:59 -0800 Subject: [PATCH] fix: prevent TypeError when ws enabled but server is undefined --- src/http-proxy-middleware.ts | 2 +- test/e2e/websocket.spec.ts | 167 +++++++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+), 1 deletion(-) diff --git a/src/http-proxy-middleware.ts b/src/http-proxy-middleware.ts index 6fe301c0..765745fa 100644 --- a/src/http-proxy-middleware.ts +++ b/src/http-proxy-middleware.ts @@ -75,7 +75,7 @@ export class HttpProxyMiddleware { this.serverOnCloseSubscribed = true; } - if (this.proxyOptions.ws === true) { + if (this.proxyOptions.ws === true && server) { // use initial request to access the server object to subscribe to http upgrade event this.catchUpgradeRequest(server); } diff --git a/test/e2e/websocket.spec.ts b/test/e2e/websocket.spec.ts index f2408cfb..50278c25 100644 --- a/test/e2e/websocket.spec.ts +++ b/test/e2e/websocket.spec.ts @@ -149,4 +149,171 @@ describe('E2E WebSocket proxy', () => { }); }); }); + + describe('ws enabled without server object (issue #143)', () => { + it('should not crash when server is undefined', async () => { + const middleware = createProxyMiddleware({ + target: `http://localhost:${WS_SERVER_PORT}`, + ws: true, + pathFilter: '/api', + }); + + // Mock request without server attached + const mockReq = { + url: '/other', // Non-matching path + headers: {}, + socket: {}, // socket without server property + } as http.IncomingMessage; + + const mockRes = { + writeHead: jest.fn(), + end: jest.fn(), + } as unknown as http.ServerResponse; + + const mockNext = jest.fn(); + + // Should not throw TypeError + await expect(async () => { + await middleware(mockReq, mockRes, mockNext); + }).resolves.not.toThrow(); + + expect(mockNext).toHaveBeenCalled(); + }); + + it('should still work when server is available', async () => { + proxyServer = createApp(proxyMiddleware).listen(SERVER_PORT); + + // Make HTTP request first + await new Promise((resolve) => http.get(`http://localhost:${SERVER_PORT}/`, resolve)); + + // WebSocket should work normally + await new Promise((resolve, reject) => { + ws = new WebSocket(`ws://localhost:${SERVER_PORT}/socket`); + ws.on('open', () => resolve()); + ws.on('error', reject); + }); + + const messageReceived = new Promise((resolve) => { + ws.on('message', (data) => resolve(data.toString())); + }); + + ws.send('test-message'); + const response = await messageReceived; + expect(response).toBe('test-message'); + }); + + it('should not crash when server is null', async () => { + const middleware = createProxyMiddleware({ + target: `http://localhost:${WS_SERVER_PORT}`, + ws: true, + pathFilter: '/api', + }); + + // Mock request with null server + const mockReq = { + url: '/other', + headers: {}, + socket: { server: null }, // explicitly null + } as unknown as http.IncomingMessage; + + const mockRes = { + writeHead: jest.fn(), + end: jest.fn(), + } as unknown as http.ServerResponse; + + const mockNext = jest.fn(); + + await expect(async () => { + await middleware(mockReq, mockRes, mockNext); + }).resolves.not.toThrow(); + + expect(mockNext).toHaveBeenCalled(); + }); + + it('should not crash on matching path with undefined server', async () => { + const middleware = createProxyMiddleware({ + target: `http://localhost:${WS_SERVER_PORT}`, + ws: true, + pathFilter: '/api', // Will not match '/test' + }); + + // Mock request with path that won't match + const mockReq = { + url: '/test', + headers: {}, + socket: {}, // no server + } as http.IncomingMessage; + + const mockRes = { + writeHead: jest.fn(), + end: jest.fn(), + } as unknown as http.ServerResponse; + + const mockNext = jest.fn(); + + // Should not throw + await expect(async () => { + await middleware(mockReq, mockRes, mockNext); + }).resolves.not.toThrow(); + + expect(mockNext).toHaveBeenCalled(); + }); + + it('should handle multiple requests with missing server', async () => { + const middleware = createProxyMiddleware({ + target: `http://localhost:${WS_SERVER_PORT}`, + ws: true, + pathFilter: '/api', + }); + + const mockNext = jest.fn(); + + // Multiple requests without server + for (let i = 0; i < 3; i++) { + const mockReq = { + url: '/other', + headers: {}, + socket: {}, + } as http.IncomingMessage; + + const mockRes = { + writeHead: jest.fn(), + end: jest.fn(), + } as unknown as http.ServerResponse; + + await expect(async () => { + await middleware(mockReq, mockRes, mockNext); + }).resolves.not.toThrow(); + } + + expect(mockNext).toHaveBeenCalledTimes(3); + }); + + it('should handle ws:false with missing server', async () => { + const middleware = createProxyMiddleware({ + target: `http://localhost:${WS_SERVER_PORT}`, + ws: false, // ws disabled + pathFilter: '/api', + }); + + const mockReq = { + url: '/other', + headers: {}, + socket: {}, // no server, but ws is disabled anyway + } as http.IncomingMessage; + + const mockRes = { + writeHead: jest.fn(), + end: jest.fn(), + } as unknown as http.ServerResponse; + + const mockNext = jest.fn(); + + await expect(async () => { + await middleware(mockReq, mockRes, mockNext); + }).resolves.not.toThrow(); + + expect(mockNext).toHaveBeenCalled(); + }); + }); });