Skip to content
Open
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: 1 addition & 1 deletion src/http-proxy-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export class HttpProxyMiddleware<TReq, TRes> {
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);
}
Expand Down
167 changes: 167 additions & 0 deletions test/e2e/websocket.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>((resolve, reject) => {
ws = new WebSocket(`ws://localhost:${SERVER_PORT}/socket`);
ws.on('open', () => resolve());
ws.on('error', reject);
});

const messageReceived = new Promise<string>((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();
});
});
});