javascriptrestclojurearchitecturewebsocket

Error handling over websockets a design decision


I'm currently building a webapp that has two clear use cases:

  1. Traditional client request data from server.
  2. Client request a stream from the server after which the server starts pushing data to the client.

Currently I'm implementing both 1 and 2 using JSON message passing over a websocket. However this has proven hard since I need to hand code lots of error handling since the client is not waiting for the response. It just sends the message hoping it will get a reply sometime. I'm using Js and react on the frontend and Clojure on the backend.

I have two questions regarding this.

  1. Given the current design, what alternatives are there for error handling over a websocket?
  2. Would it be smarter to split the two UC using rest for UC1 and websockets for UC2 then I could use something like fetch on the frontend for rest calls.

Update. The current problem is not knowing how to build an async send function over websockets can match send messages and response messages.


Solution

  • Here's a scheme for doing request/response over socket.io. You could do this over plain webSocket, but you'd have to build a little more of the infrastructure yourself. This same library can be used in client and server:

    function initRequestResponseSocket(socket, requestHandler) {
        var cntr = 0;
        var openResponses = {};
    
        // send a request
        socket.sendRequestResponse = function(data, fn) {
            // put this data in a wrapper object that contains the request id
            // save the callback function for this id
            var id = cntr++;
            openResponses[id] = fn;
            socket.emit('requestMsg', {id: id, data: data});
        }
    
        // process a response message that comes back from a request
        socket.on('responseMsg', function(wrapper) {
            var id = wrapper.id, fn;
            if (typeof id === "number" && typeof openResponses[id] === "function") {
                fn = openResponses[id];
                delete openResponses[id];
                fn(wrapper.data);
            }
        });
    
        // process a requestMsg
        socket.on('requestMsg', function(wrapper) {
            if (requestHandler && wrapper.id) {
                requestHandler(wrapper.data, function(responseToSend) {
                    socket.emit('responseMsg', {id: wrapper.id, data; responseToSend});
                });
            }
        });
    }
    

    This works by wrapping every message sent in a wrapper object that contains a unique id value. Then, when the other end sends it's response, it includes that same id value. That id value can then be matched up with a particular callback response handler for that specific message. It works both ways from client to server or server to client.

    You use this by calling initRequestResponseSocket(socket, requestHandler) once on a socket.io socket connection on each end. If you wish to receive requests, then you pass a requestHandler function which gets called each time there is a request. If you are only sending requests and receiving responses, then you don't have to pass in a requestHandler on that end of the connection.

    To send a message and wait for a response, you do this:

    socket.sendRequestResponse(data, function(err, response) {
        if (!err) {
            // response is here
        }
    });
    

    If you're receiving requests and sending back responses, then you do this:

    initRequestResponseSocket(socket, function(data, respondCallback) {
       // process the data here
    
       // send response
       respondCallback(null, yourResponseData);
    });
    

    As for error handling, you can monitor for a loss of connection and you could build a timeout into this code so that if a response doesn't arrive in a certain amount of time, then you'd get an error back.

    Here's an expanded version of the above code that implements a timeout for a response that does not come within some time period:

    function initRequestResponseSocket(socket, requestHandler, timeout) {
        var cntr = 0;
        var openResponses = {};
    
        // send a request
        socket.sendRequestResponse = function(data, fn) {
            // put this data in a wrapper object that contains the request id
            // save the callback function for this id
            var id = cntr++;
            openResponses[id] = {fn: fn};
            socket.emit('requestMsg', {id: id, data: data});
            if (timeout) {
                openResponses[id].timer = setTimeout(function() {
                    delete openResponses[id];
                    if (fn) {
                        fn("timeout");
                    }
                }, timeout);
            }
        }
    
        // process a response message that comes back from a request
        socket.on('responseMsg', function(wrapper) {
            var id = wrapper.id, requestInfo;
            if (typeof id === "number" && typeof openResponse[id] === "object") {
                requestInfo = openResponses[id];
                delete openResponses[id];
                if (requestInfo) {
                    if (requestInfo.timer) {
                        clearTimeout(requestInfo.timer);
                    }
                    if (requestInfo.fn) {
                        requestInfo.fn(null, wrapper.data);
                    }
                }
            }
        });
    
        // process a requestMsg
        socket.on('requestMsg', function(wrapper) {
            if (requestHandler && wrapper.id) {
                requestHandler(wrapper.data, function(responseToSend) {
                    socket.emit('responseMsg', {id: wrapper.id, data; responseToSend});
                });
    
            }
        });
    }