HTTP Proxy/Load Balancer in Node.js

There are different existing mature reverse proxy servers and load balancers like NGINX or Traefik.

But imagine I have a situation where I need to redirect requests coming to my server to single or multiple (load balancing) locations and existing products do not suit my needs.

For example, because of one of the following:

  1. need for some custom pre- or post-processing to be applied in request or response
  2. need for some custom load-balancing logic (e.g. depending on readiness or state of balanced microservices etc.)
  3. need for custom authentication or request filtering solution
  4. need to intercept and log all the requests and responses between 3rd party components
  5. need to create some adapter between client and server, where client expects specific format of server API and server provides it in another format (so, you need to convert requests and responses on the fly)
  6. and more…

In this case, I would need to write the proxy/load balancer by myself – and it is interesting and not complicated task.

In the past I wrote proxy server using C# Reverse proxy with requests redirect in C# , this time I will use node.js which is ideal for this task.

So, here we are. The code is pretty short, simple and self-explanatory:

var express = require('express');
var app = express();
var http = require('http');
 
// ====================================================================
// Read settings
 
var PORT = process.env.PORT || 8080;
 
// Array of target addresses
var targetAddresses = [];
var targetAddressesStr = process.env.TARGET_ADDRESSES || JSON.stringify({"targetAddresses":["http://www.washington.edu/","http://www.WASHINGTON.edu/","http://www.washington.EDU/"]});
if(targetAddressesStr){
    var targetAddressesJson = JSON.parse(targetAddressesStr);
    targetAddresses = targetAddressesJson.targetAddresses;
}

// Printing statistics of number of requests/minute
var requestsPerMinute = 0;
setInterval(function(){
    console.log("Proxied requests per minute: " + requestsPerMinute);
    requestsPerMinute = 0;
    }, 60000);
 
console.log(`http_proxy init: targetAddresses=${JSON.stringify(targetAddresses)} port=${PORT}`);
 
// ====================================================================
 
var currentTargetAddressIndex = 0;
 
/**
* Function returns next available ready target address,
* performing round robin iteration over available targets
* @returns {null|*}
*/
function getAvailableTargetAddress(){
    // select the suitable array of target addresses
    var activeTargetAddresses = targetAddresses;
    if(activeTargetAddresses.length==0)
        return null;
    // select next address using round robin
    if(currentTargetAddressIndex>=activeTargetAddresses.length)
        currentTargetAddressIndex = 0;
    var s = activeTargetAddresses[currentTargetAddressIndex];
    currentTargetAddressIndex++;
    if(currentTargetAddressIndex>=activeTargetAddresses.length)
        currentTargetAddressIndex = 0;
 
    return s;
}

// For "http://aaa.bbb:xxx" returns aaa.bbb
function getUrlHost(url){
    var surl = url.replace("http://","");
    var i1 = surl.indexOf(":");
    var i2 = surl.indexOf("/");
    var mini = 0;
    if(i1>0 && i2>0)
        mini = i1>i2?i2:i1;
    else if(i1<0)
        mini = i2;
    else
        mini = i1;
    if(mini>0) {
        var s = surl.substr(0, mini);
        return s;
    }
    return surl;
}
// For "http://aaa.bbb:xxx" returns xxx
function getUrlPort(url){
    var surl = url.replace("http://","");
    var s = surl.split(":");
    if(s.length>1)
        return s[1];
    else
        return 80;
}
 
// ====================================================================

// Listen to incoming requests 
app.use('/', function(clientRequest, clientResponse) {
    console.log(`Got request: ${clientRequest.url} ${clientRequest.method}`);
    requestsPerMinute++;

    // read client request body
    var clientBodyChunks = [];
    clientRequest.on('data', (chunk)=>{
        console.log(`Got request: ${clientRequest.url} ${clientRequest.method} body data chunk received`);
        clientBodyChunks.push(chunk);
    });
    clientRequest.on('end', (end)=>{
        console.log(`Got request: ${clientRequest.url} ${clientRequest.method} body data end received`);
        try {
            var clientBodyData = Buffer.concat(clientBodyChunks);
 
            var requestUrl = clientRequest.url;
            // Select the server to redirect to
            var hostUrl = getAvailableTargetAddress();
            if (!hostUrl) {
                console.error(`Request ${clientRequest.url} - no available ready targets`);
                clientResponse.status(503).send("Proxy: Service unavailable, no ready targets.");
                return;
            }

            // create secondary request to proxied server, copy original body and headers
            var url = hostUrl;
            if (requestUrl && requestUrl.length > 0 && requestUrl != "/")
                url = url + requestUrl;
            console.log("Redirecting to : " + url);
 
            var options = {
                hostname: getUrlHost(hostUrl),
                port: getUrlPort(hostUrl),
                path: requestUrl,
                method: clientRequest.method,
                headers: clientRequest.headers
            };
 
            // Send the request
            var serverRequest = http.request(options, function (serverResponse) {
                console.log("Response: " + clientRequest.url + " status: " + serverResponse.statusCode);

                // gather all response chunks 
                var bodyChunks = [];
                serverResponse.on('data', function (chunk) {
                    bodyChunks.push(chunk);
                });
                // on end of response transmission - concatenate all chunks and send response to the original caller
                serverResponse.on('end', function () {
                    var data = Buffer.concat(bodyChunks);
                    clientResponse.writeHead(serverResponse.statusCode, serverResponse.headers);
                    clientResponse.end(data);
                    console.log("Finished: " + clientRequest.url);
                });
            });
            serverRequest.on('error', function (e, req) {
                console.error("Error for " + url + " : " + e.message);
                clientResponse.status(500).send("Proxy: " + e.message);
            });
            if (clientBodyData)
                serverRequest.write(clientBodyData);
            serverRequest.end();
        }
        catch(Error){
            console.error(`Request processing exception: ${clientRequest.url} : ${Error}`);
            clientResponse.status(500).send("Proxy: exception: " + Error.message);
        }
    });
});
 
app.listen(PORT);
console.log(`web_proxy listening to port ${PORT}`);

We listen to incoming requests, choose the right server, forward the request to this server, get the response, return the response to the original caller.

For simplicity, I configure the proxy to use 3 server addresses which are actually the same server http://www.washington.edu/, just with different letter case – you may see in the log that different addresses are selected:

Leave a comment