Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handle http server crashes

I have a very basic http server:

require("http").createServer(function (req, res) {
    res.end("Hello world!");                      
}).listen(8080);                                 

How can I listen for server crashes so I can send a 500 status code in response?

Listening for process.on("uncaughtException", handler) works at process level, but I don't have the request and response objects.

A possible solution I see is using try - catch statements inside of createServer callback, but I'm looking if there are better solutions.

I tried listening for error event on server object, but nothing happens:

var s = require("http").createServer(function (req, res) {
    undefined.foo; // test crash
    res.end("Hello world!");                      
});
s.on("error", function () { console.log(arguments); });
s.listen(8080);                                 
like image 804
Ionică Bizău Avatar asked Sep 21 '14 08:09

Ionică Bizău


People also ask

Why does my server keep crashing?

Sometimes poorly optimized server-side code can consume all your resources under normal operating conditions and cause your server to become overwhelmed and crash. Or the frontend code itself might be the source of the crash. If there's an infinite loop or you aren't handling all edge cases, your server could go down.

What happens when server crashes?

1. What is a server crash? Server crashes occur when a website, software application, or operating system stops functioning and does not get displayed. Servers are nothing but hardware infused with a lot of software programs incorporating lists of codes necessary to make data (website/application) available for users.

How do I stop a node JS process from crashing?

Always handle promise rejections. Listen to unhandled rejection events for crash reporting. And always use errors to reject promises in order to get stack traces! Always handle the error event for streams!


1 Answers

Catching and handling the error

You can use node's built-in domain module for this.

Domains provide a way to handle multiple different IO operations as a single group. If any of the event emitters or callbacks registered to a domain emit an error event, or throw an error, then the domain object will be notified, rather than losing the context of the error in the process.on('uncaughtException') handler, or causing the program to exit immediately with an error code.

One very important thing to note is this:

Domain error handlers are not a substitute for closing down your process when an error occurs.

By the very nature of how throw works in JavaScript, there is almost never any way to safely "pick up where you left off", without leaking references, or creating some other sort of undefined brittle state.

Since you're only asking about how to respond with a 500 error, I'm not going to go in to how to deal with restarting the server, etc. like the node documentation does; I highly recommend taking a look at the example in the node docs. Their example shows how to capture the error, send an error response back to the client (if possible), and then restart the server. I'll just show the domain creation and sending back a 500 error response. (see next section regarding restarting the process)

Domains work similarly to putting a try/catch in your createServer callback. In your callback:

  1. Create a new domain object
  2. Listen on the domain's error event
  3. Add req and res to the domain (since they were created before the domain existed)
  4. run the domain and call your request handler (this is like the try part of a try/catch)

Something like this:

var domain = require('domain');

function handleRequest(req, res) {
    // Just something to trigger an async error
    setTimeout(function() {
        throw Error("Some random async error");
        res.end("Hello world!");  
    }, 100);
}

var server = require("http").createServer(function (req, res) {
    var d = domain.create();

    d.on('error', function(err) {
        // We're in an unstable state, so shutdown the server.
        // This will only stop new connections, not close existing ones.
        server.close();

        // Send our 500 error
        res.statusCode = 500;
        res.setHeader("content-type", "text/plain");
        res.end("Server error: " + err.message);
    });

    // Since the domain was created after req and res, they
    // need to be explictly added.
    d.add(req);
    d.add(res);

    // This is similar to a typical try/catch, but the "catch"
    // is now d's error event.
    d.run(function() {
        handleRequest(req, res);
    });
}).listen(8080); 

Restarting the process after an error

By using the cluster module you can nicely restart the process after an error. I'm basically copying the example from the node documentation here, but the general idea is to start multiple worker processes from a master process. The workers are the processes that handle the incoming connections. If one of them has an unrecoverable error (i.e. the ones we're catching in the previous section), then it'll disconnect from the master process, send a 500 response, and exit. When the master process sees the worker process disconnect, it'll know that an error occurred and spin up a new worker. Since there are multiple worker processes running at once, there shouldn't be an issue with missing incoming connections if one of them goes down.

Example code, copied from here:

var cluster = require('cluster');
var PORT = +process.env.PORT || 1337;

if (cluster.isMaster) {
  // In real life, you'd probably use more than just 2 workers,
  // and perhaps not put the master and worker in the same file.
  //
  // You can also of course get a bit fancier about logging, and
  // implement whatever custom logic you need to prevent DoS
  // attacks and other bad behavior.
  //
  // See the options in the cluster documentation.
  //
  // The important thing is that the master does very little,
  // increasing our resilience to unexpected errors.

  cluster.fork();
  cluster.fork();

  cluster.on('disconnect', function(worker) {
    console.error('disconnect!');
    cluster.fork();
  });

} else {
  // the worker
  //
  // This is where we put our bugs!

  var domain = require('domain');

  // See the cluster documentation for more details about using
  // worker processes to serve requests.  How it works, caveats, etc.

  var server = require('http').createServer(function(req, res) {
    var d = domain.create();
    d.on('error', function(er) {
      console.error('error', er.stack);

      // Note: we're in dangerous territory!
      // By definition, something unexpected occurred,
      // which we probably didn't want.
      // Anything can happen now!  Be very careful!

      try {
        // make sure we close down within 30 seconds
        var killtimer = setTimeout(function() {
          process.exit(1);
        }, 30000);
        // But don't keep the process open just for that!
        killtimer.unref();

        // stop taking new requests.
        server.close();

        // Let the master know we're dead.  This will trigger a
        // 'disconnect' in the cluster master, and then it will fork
        // a new worker.
        cluster.worker.disconnect();

        // try to send an error to the request that triggered the problem
        res.statusCode = 500;
        res.setHeader('content-type', 'text/plain');
        res.end('Oops, there was a problem!\n');
      } catch (er2) {
        // oh well, not much we can do at this point.
        console.error('Error sending 500!', er2.stack);
      }
    });

    // Because req and res were created before this domain existed,
    // we need to explicitly add them.
    // See the explanation of implicit vs explicit binding below.
    d.add(req);
    d.add(res);

    // Now run the handler function in the domain.
    d.run(function() {
      handleRequest(req, res);
    });
  });
  server.listen(PORT);
}

// This part isn't important.  Just an example routing thing.
// You'd put your fancy application logic here.
function handleRequest(req, res) {
  switch(req.url) {
    case '/error':
      // We do some async stuff, and then...
      setTimeout(function() {
        // Whoops!
        flerb.bark();
      });
      break;
    default:
      res.end('ok');
  }
}

Note: I still want to stress that you should take a look at the domain module documentation and look at the examples and explanations there. It explains most, if not all, of this, the reasoning behind it, and some other situations you might run in to.

like image 66
Mike S Avatar answered Sep 22 '22 07:09

Mike S