Skip to content

Commit da134d0

Browse files
phoclaude
andcommitted
fix: resolve SignalR proxy memory leak by creating middleware once
Fixed MaxListenersExceededWarning errors (11+ listeners on Socket objects) that occurred when refreshing the page with F5. Root Cause: - createProxyMiddleware was instantiated on every request to /api/signalr - Each instance attached event listeners (error, close, unpipe, finish) to Socket objects - Multiple concurrent requests during page refresh created multiple proxy instances - Event listeners accumulated without cleanup, triggering Node.js memory leak warnings Solution: - Moved createProxyMiddleware instantiation to server startup (once) - Created singleton signalrProxy instance - Reused same instance for all requests and WebSocket upgrades - Proxy middleware now properly manages its own listener lifecycle Result: - Event listeners remain constant (4 listeners per proxy instance) - No more MaxListenersExceededWarning when refreshing - Proper cleanup of connections handled by single middleware instance Fixes: MaxListenersExceededWarning: Possible EventTarget memory leak detected 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 21acbb7 commit da134d0

File tree

1 file changed

+28
-40
lines changed

1 file changed

+28
-40
lines changed

frontend/server.js

Lines changed: 28 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,32 @@ const app = next({ dev, hostname, port })
1111
const handle = app.getRequestHandler()
1212

1313
app.prepare().then(() => {
14+
// Create SignalR proxy middleware ONCE at startup to prevent memory leaks
15+
// Creating it per-request causes event listener accumulation on Socket objects
16+
const backendUrl = process.env.SIGNALR_URL || 'http://localhost:5000'
17+
const signalrProxy = createProxyMiddleware({
18+
target: backendUrl,
19+
changeOrigin: true,
20+
ws: true, // Enable WebSocket proxying
21+
pathRewrite: {
22+
'^/api/signalr': '/hubs', // Rewrite /api/signalr/* to /hubs/*
23+
},
24+
onProxyReq: (proxyReq, req) => {
25+
// Add custom headers if needed
26+
proxyReq.setHeader('X-Forwarded-For', req.socket.remoteAddress)
27+
proxyReq.setHeader('X-Forwarded-Proto', 'http')
28+
proxyReq.setHeader('X-Forwarded-Host', req.headers.host)
29+
},
30+
onError: (err, req, res) => {
31+
console.error('SignalR Proxy Error:', err)
32+
if (res.writeHead) {
33+
res.writeHead(500, { 'Content-Type': 'application/json' })
34+
res.end(JSON.stringify({ error: 'Proxy error', details: err.message }))
35+
}
36+
},
37+
logLevel: dev ? 'debug' : 'warn',
38+
})
39+
1440
const server = createServer(async (req, res) => {
1541
try {
1642
const parsedUrl = parse(req.url, true)
@@ -22,34 +48,8 @@ app.prepare().then(() => {
2248
return handle(req, res, parsedUrl)
2349
}
2450

25-
// Proxy SignalR WebSocket connections to backend
51+
// Proxy SignalR WebSocket connections to backend using the singleton proxy
2652
if (pathname && pathname.startsWith('/api/signalr')) {
27-
const backendUrl = process.env.SIGNALR_URL || 'http://localhost:5000'
28-
29-
// Create proxy middleware for SignalR
30-
const signalrProxy = createProxyMiddleware({
31-
target: backendUrl,
32-
changeOrigin: true,
33-
ws: true, // Enable WebSocket proxying
34-
pathRewrite: {
35-
'^/api/signalr': '/hubs', // Rewrite /api/signalr/* to /hubs/*
36-
},
37-
onProxyReq: (proxyReq, req) => {
38-
// Add custom headers if needed
39-
proxyReq.setHeader('X-Forwarded-For', req.socket.remoteAddress)
40-
proxyReq.setHeader('X-Forwarded-Proto', 'http')
41-
proxyReq.setHeader('X-Forwarded-Host', req.headers.host)
42-
},
43-
onError: (err, req, res) => {
44-
console.error('SignalR Proxy Error:', err)
45-
if (res.writeHead) {
46-
res.writeHead(500, { 'Content-Type': 'application/json' })
47-
res.end(JSON.stringify({ error: 'Proxy error', details: err.message }))
48-
}
49-
},
50-
logLevel: dev ? 'debug' : 'warn',
51-
})
52-
5353
return signalrProxy(req, res)
5454
}
5555

@@ -62,23 +62,11 @@ app.prepare().then(() => {
6262
}
6363
})
6464

65-
// Handle WebSocket upgrade requests
65+
// Handle WebSocket upgrade requests using the singleton proxy
6666
server.on('upgrade', (req, socket, head) => {
6767
const { pathname } = parse(req.url, true)
6868

6969
if (pathname && pathname.startsWith('/api/signalr')) {
70-
const backendUrl = process.env.SIGNALR_URL || 'http://localhost:5000'
71-
72-
const signalrProxy = createProxyMiddleware({
73-
target: backendUrl,
74-
changeOrigin: true,
75-
ws: true,
76-
pathRewrite: {
77-
'^/api/signalr': '/hubs',
78-
},
79-
logLevel: dev ? 'debug' : 'warn',
80-
})
81-
8270
signalrProxy.upgrade(req, socket, head)
8371
} else {
8472
socket.destroy()

0 commit comments

Comments
 (0)