Reputation: 147
My project requires that I send information to the front-end concerning the status of a transaction so the client will be notified. I began to do this using Server Sent Events and I had issues with firefox be unstable though messages came through. My project lead asked me to use websockets since all browsers support it. I followed Heroku's directions on how to implement it after I got it working on my local development server. I noticed the client return blocked and in Heroku logs its says
[router]: at=info method=GET path="/api/v1/ws" host=dreamjobs-api-2de3c2827093.herokuapp.com request_id=dd486341-52db-4493-837d-0791fa443127 fwd="154.160.14.153" dyno=web.1 connect=0ms service=1ms status=404 bytes=964 protocol=https
I was thinking there has to be some configuration for Heroku to enable that communication so I searched online and found that I need to log in to Heroku CLI and send this heroku features:enable http-session-affinity -a your-app-name
. I did just that but the websocket server is still not running in Heroku. Please analyze my code because I believe I did all I was directed to on Heroku's instruction on implementing this feature. Any attempts will be appreciated.
import express, { Request, Response } from 'express';
import cors from 'cors';
import bodyParser from 'body-parser';
import helmet from 'helmet';
import prisma from './lib/prisma';
import { Socket } from 'net';
import { createServer, IncomingMessage } from 'http';
import WebSocket, { Server as WebSocketServer } from 'ws';
//other imports for routes...
interface ExtendedWebSocket extends WebSocket {
isAlive: boolean;
}
const port = process.env.PORT || 8080;
const app = express();
const server = createServer(app);
async function main() {
// app.use(express.json());
app.use(
cors({
origin: function (origin, callback) {
const allowedOrigins = ['https://www.myfrontend.com', 'https://admin.myfrontend.com'];
if (!origin || allowedOrigins.includes(origin) || origin.startsWith('ws://') || origin.startsWith('wss://')) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
optionsSuccessStatus: 200,
}),
);
app.use(helmet());
app.use(bodyParser.urlencoded({ limit: '100mb', extended: false }));
app.use(bodyParser.json({ limit: '100mb' }));
// Create a WebSocket server
// const wss: WebSocketServer = new WebSocketServer({ port: 1280 });
const wss: WebSocketServer = new WebSocketServer({ server });
// A map to store connected clients
const clients: Map<string, ExtendedWebSocket> = new Map();
wss.on('connection', (ws: ExtendedWebSocket) => {
//Error handling
ws.on('error', (error: Error) => {
console.log('error', error);
});
// Generate a unique ID for the client and add it to the map
const clientId = Date.now();
clients.set(clientId.toString(), ws);
console.log('Client connected:', clientId);
// Implement ping-pong to keep connection alive
ws.isAlive = true;
ws.on('pong', () => {
ws.isAlive = true;
});
// Handle incoming messages from the client
ws.on('message', (message: string) => {
console.log('message', message);
});
// Send a message to the client
const systemMsg: { id: string; message: string } = { id: clientId.toString(), message: 'Connected' };
ws.send(JSON.stringify(systemMsg));
// Clean up when the client disconnects
ws.on('close', () => {
console.log('Client disconnected:', clientId);
clients.delete(clientId.toString());
});
});
// Implement ping-pong interval
const interval = setInterval(() => {
wss.clients.forEach((ws: WebSocket) => {
const extendedWs = ws as ExtendedWebSocket;
if (extendedWs.isAlive === false) return ws.terminate();
extendedWs.isAlive = false;
ws.ping(() => {
ws.pong();
});
});
}, 30000);
wss.on('close', () => {
clearInterval(interval);
});
// Upgrade HTTP server to WebSocket server
server.on('upgrade', (request: IncomingMessage, socket: Socket, head: Buffer) => {
socket.on('error', (error) => {
console.error('Socket error:', error);
});
if (request.headers.upgrade === 'websocket' && request.url === 'api/v1/ws') {
wss.handleUpgrade(request, socket, head, (ws: WebSocket) => {
socket.removeListener('error', (error) => console.log(`error ${error}`));
wss.emit('connection', ws, request);
});
} else {
socket.destroy(); // Not a WebSocket handshake
}
});
//event webhook
app.post('/api/v1/payment/momo-event-webhook', async (req: Request, res: Response) => {
const event = req.body;
// Relay the event to all connected clients
for (const [clientId, clientWs] of clients.entries()) {
console.log('Sending event to client', clientId);
clientWs.send(JSON.stringify({ event, clientId }));
}
res.status(200).json({ success: true, message: 'Event received' });
});
// Register API routes
//....
// Specific CORS configuration for the webhook route
const webhookCorsOptions = {
origin: '*', // Allow from all origins, or specify the exact origin like 'https://payment-gateway.com'
methods: ['GET', 'POST'], // Allow POST and GET requests
allowedHeaders: ['Content-Type'],
};
// Webhook route
app.use('/api/v1/payment/momo-response-webhook', cors(webhookCorsOptions), (req: Request, res: Response) => {
// Handle the webhook request
console.log('Webhook received:', req.body);
res.status(200).json({ success: true, message: 'Webhook received' });
});
// Catch unregistered routes
app.all('*', (req: Request, res: Response) => {
res.status(404).json({ error: `Route ${req.originalUrl} not found` });
});
app.listen(port, () => {
console.log(`Server is listening on port ${port}`);
});
}
main()
.then(async () => {
await prisma.$connect();
})
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});
Steps to reproduce
Upvotes: 0
Views: 67
Reputation: 147
It was a post I saw on this platform that gave me an idea on how to approach this situation. So in 4 steps this issue was solved
step 1. Create an interface to extend Request object
interface WebSocketServerRequest extends Request {
wss?: WebSocketServer;
}
step 2. Create a middleware to use the extended Request and place websocket instance in to request object
const addWebSocketServer = (wss: WebSocketServer) => {
return (req: WebSocketServerRequest, res: Response, next: NextFunction) => {
req.wss = wss;
next();
};
};
step 3. Use the middleware
app.use(addWebSocketServer(wss));
step 4.Create a route handler for websocket and in it upgrade the server to use websocket protocol
app.get('/api/v1/ws', (req: WebSocketServerRequest, res: Response) => {
const wss = req.wss;
if (wss) {
//Request websocket upgrade
wss.handleUpgrade(req, req.socket, Buffer.alloc(0), (ws) => {
wss.emit('connection', ws, req);
});
// Handle the WebSocket request
console.log('WebSocket server available');
} else {
res.status(400).json({ error: 'WebSocket server not available' });
}
});
This has resolved the issue and I can connect to websocket server on Heroku but there is an issue of disconnection after about 40 sec after the connection is made. Error says Invalid WebSocket frame: FIN must be set
I have looked online but no results so I have posted it in the github page of ws
Upvotes: 0