web-dev-qa-db-fra.com

traitement des erreurs dans les appels asynchrones de node.js

Je suis nouveau sur node.js bien que je sois assez familier avec JavaScript en général. Ma question concerne les "meilleures pratiques" sur la manière de gérer les erreurs dans node.js.

Normalement, lors de la programmation de serveurs Web, de serveurs FastCGI ou de pages Web dans différentes langues, j'utilise Exceptions avec des gestionnaires de blocage dans un environnement multithread. Quand une demande arrive, je fais habituellement quelque chose comme ça:

function handleRequest(request, response) {
  try {

    if (request.url=="whatever")
      handleWhateverRequest(request, response);
    else
      throw new Error("404 not found");

  } catch (e) {
    response.writeHead(500, {'Content-Type': 'text/plain'});
    response.end("Server error: "+e.message);
  }
}

function handleWhateverRequest(request, response) {
  if (something) 
    throw new Error("something bad happened");
  Response.end("OK");
}

De cette façon, je peux toujours gérer les erreurs internes et envoyer une réponse valide à l'utilisateur.

Je comprends qu'avec node.js, on est supposé faire des appels non bloquants, ce qui conduit évidemment à un nombre différent de rappels, comme dans cet exemple:

var sys    = require('sys'),
    fs     = require('fs');

require("http").createServer(handleRequest).listen(8124);

function handleRequest(request, response) {

  fs.open("/proc/cpuinfo", "r",
    function(error, fd) {
      if (error)
        throw new Error("fs.open error: "+error.message);

      console.log("File open.");

      var buffer = new require('buffer').Buffer(10);
      fs.read(fd, buffer, 0, 10, null,
        function(error, bytesRead, buffer) {

          buffer.dontTryThisAtHome();  // causes exception

          response.end(buffer);
        }); //fs.read

    }); //fs.open

}

Cet exemple tuera le serveur complètement parce que des exceptions ne sont pas capturées . Mon problème est que je ne peux plus utiliser un seul test/catch et que par conséquent, je ne peux généralement pas détecter les erreurs qui pourraient être générées lors du traitement de la demande. 

Bien sûr, je pourrais ajouter un essai/attraper à chaque rappel mais je n'aime pas cette approche, car c'est ensuite au programmeur qu'il ne doit pas oublier essayer/attraper. Ce n'est pas acceptable pour un serveur complexe avec beaucoup de gestionnaires différents et complexes.

Je pourrais utiliser un gestionnaire d'exception global (empêchant le crash complet du serveur), mais je ne peux pas envoyer de réponse à l'utilisateur car je ne sais pas quelle demande a entraîné l'exception. Cela signifie également que la demande reste non gérée/ouverte et que le navigateur attend une réponse pour toujours.

Quelqu'un a-t-il une bonne solution solide?

45
Udo G

Le nœud 0.8 introduit un nouveau concept appelé "Domaines". Elles ressemblent beaucoup à AppDomains dans .net et fournissent un moyen d'encapsuler un groupe d'opérations IO. Ils vous permettent en principe de regrouper vos appels de traitement de demandes dans un groupe spécifique au contexte. Si ce groupe lève des exceptions non capturées, elles peuvent être traitées et traitées de manière à vous donner accès à toutes les informations spécifiques à la portée et au contexte nécessaires pour pouvoir résoudre l'erreur (si possible).

Cette fonctionnalité est nouvelle et vient tout juste d'être introduite, donc utilisez-la avec prudence, mais d'après ce que je peux dire, elle a été spécifiquement introduite pour traiter le problème que le PO tente de résoudre.

La documentation peut être trouvée à: http://nodejs.org/api/domain.html

13
Sam Shiles

C'est l'un des problèmes avec Node en ce moment. Il est pratiquement impossible de déterminer quelle demande a provoqué une erreur dans un rappel. 

Vous allez devoir gérer vos erreurs dans les rappels eux-mêmes (où vous avez toujours une référence aux objets de demande et de réponse), si possible. Le gestionnaire uncaughtException empêchera le processus de noeud de quitter, mais la demande qui a provoqué l'exception en premier lieu sera simplement suspendue du point de vue de l'utilisateur.

5
chjj

Extraire le gestionnaire uncaughtException dans node.js. Il capture les erreurs renvoyées qui remontent dans la boucle d'événements.

http://nodejs.org/docs/v0.4.7/api/process.html#event_uncaughtException_

Mais ne pas lancer d'erreurs est toujours une meilleure solution. Vous pouvez simplement faire un return res.end('Unabled to load file xxx');

5
3rdEden

Je réponds à ma propre question ... :)

Comme il semble qu’il n’y ait aucun moyen de détecter les erreurs manuellement. J'utilise maintenant une fonction d'assistance qui renvoie elle-même un function contenant un bloc try/catch. De plus, ma propre classe de serveur Web vérifie si la fonction de traitement de la demande appelle response.end() ou la fonction try/catch helper waitfor() (sinon, déclenche une exception). Cela évite dans une large mesure que les demandes soient laissées par erreur sans protection par le développeur. Ce n'est pas une solution sujette aux erreurs à 100% mais fonctionne assez bien pour moi.

handler.waitfor = function(callback) {
  var me=this;

  // avoid exception because response.end() won't be called immediately:
  this.waiting=true;

  return function() {
    me.waiting=false;
    try {
      callback.apply(this, arguments);

      if (!me.waiting && !me.finished)
        throw new Error("Response handler returned and did neither send a "+
          "response nor did it call waitfor()");

    } catch (e) {
      me.handleException(e);
    }
  }
}

De cette façon, je dois juste ajouter un appel waitfor() en ligne pour être du bon côté.

function handleRequest(request, response, handler) {
  fs.read(fd, buffer, 0, 10, null, handler.waitfor(
    function(error, bytesRead, buffer) {

      buffer.unknownFunction();  // causes exception
      response.end(buffer);
    }
  )); //fs.read
}

Le mécanisme de vérification actuel est un peu plus complexe, mais son fonctionnement devrait être clair. Si quelqu'un est intéressé, je peux poster le code complet ici.

3
Udo G

Très bonne question. Je traite le même problème maintenant. Le meilleur moyen serait probablement d'utiliser uncaughtException. La référence aux objets respone et request n'est pas le problème, car vous pouvez les encapsuler dans votre objet exception, qui est passé à l'événement uncaughtException. Quelque chose comme ça:

var HttpException = function (request, response, message, code) {

  this.request = request;
  this.response = response;  
  this.message = message;    
  this.code = code || 500;

}

Jetez le:

throw new HttpException(request, response, 'File not found', 404);

Et gérer la réponse:

process.on('uncaughtException', function (exception) {
  exception.response.writeHead(exception.code, {'Content-Type': 'text/html'});
  exception.response.end('Error ' + exception.code + ' - ' + exception.message);
});

Je n'ai pas encore testé cette solution, mais je ne vois pas pourquoi cela ne fonctionnerait pas.

3
Marian Galik

Une idée: vous pouvez simplement utiliser une méthode d'assistance pour créer vos rappels et en faire votre pratique habituelle. Cela alourdit encore le développeur, mais au moins vous pouvez avoir une façon "standard" de gérer vos rappels, de sorte que les chances d’en oublier un sont faibles:

var callWithHttpCatch = function(response, fn) {
    try {
        fn && fn();
    }
    catch {
        response.writeHead(500, {'Content-Type': 'text/plain'}); //No
    }
}

<snipped>
      var buffer = new require('buffer').Buffer(10);
      fs.read(fd, buffer, 0, 10, null,
        function(error, bytesRead, buffer) {

          callWithHttpCatch(response, buffer.dontTryThisAtHome());  // causes exception

          response.end(buffer);
        }); //fs.read

    }); //fs.open

Je sais que ce n’est probablement pas la réponse que vous recherchiez, mais l’un des points positifs de ECMAScript (ou de la programmation fonctionnelle en général) est la facilité avec laquelle vous pouvez utiliser vos propres outils pour de telles choses.

2
jslatts

Au moment d'écrire ces lignes, l'approche que je vois est d'utiliser les "promesses".

http://howtonode.org/promises
https://www.promisejs.org/

Celles-ci permettent au code et aux rappels d'être bien structurés pour la gestion des erreurs et le rendent également plus lisible. Il utilise principalement la fonction .then ().

someFunction().then(success_callback_func, failed_callback_func);

Voici un exemple de base:

  var SomeModule = require('someModule');

  var success = function (ret) {
      console.log('>>>>>>>> Success!');
  }

  var failed = function (err) {
    if (err instanceof SomeModule.errorName) {
      // Note: I've often seen the error definitions in SomeModule.errors.ErrorName
      console.log("FOUND SPECIFIC ERROR");
    }
    console.log('>>>>>>>> FAILED!');
  }

  someFunction().then(success, failed);
  console.log("This line with appear instantly, since the last function was asynchronous.");
1
SilentSteel

De même, dans la programmation multi-thread synchrone (par exemple .NET, Java, PHP), vous ne pouvez renvoyer aucune information utile au client lorsqu'une exception personnalisée non connue est interceptée. Vous pouvez simplement renvoyer HTTP 500 lorsque vous ne disposez d'aucune information concernant l'exception.

Ainsi, le "secret" réside dans le remplissage d'un objet Error descriptif, ainsi votre gestionnaire d'erreurs peut mapper de l'erreur significative au bon statut HTTP + éventuellement un résultat descriptif. Cependant, vous devez également intercepter l'exception avant qu'elle n'arrive sur process.on ('uncaughtException'):

Étape 1: Définir un objet d'erreur significatif

function appError(errorCode, description, isOperational) {
    Error.call(this);
    Error.captureStackTrace(this);
    this.errorCode = errorCode;
    //...other properties assigned here
};

appError.prototype.__proto__ = Error.prototype;
module.exports.appError = appError;

Étape 2: lors du lancement d'une exception, remplissez-la avec les propriétés (voir l'étape 1) permettant au gestionnaire de la convertir en résultat HTTP meannigul:

throw new appError(errorManagement.commonErrors.resourceNotFound, "further explanation", true)

Étape 3: Lorsque vous appelez du code potentiellement dangereux, corrigez les erreurs et relancez cette erreur tout en remplissant des propriétés contextuelles supplémentaires dans l'objet Error

Étape 4: Vous devez intercepter l'exception lors du traitement de la demande. Ceci est plus facile si vous utilisez une bibliothèque de promesses de premier plan (BlueBird est génial) qui vous permet de détecter les erreurs asynchrones. Si vous ne pouvez pas utiliser les promesses, une bibliothèque NODE intégrée renverra des erreurs de rappel.

Étape 5: Maintenant que votre erreur est interceptée et qu'elle contient des informations descriptives sur ce qui se passe, il vous suffit de la mapper à une réponse HTTP significative. La partie Nice ici est que vous pouvez avoir un gestionnaire d’erreurs unique centralisé qui récupère toutes les erreurs et les mappe à la réponse HTTP:

    //this specific example is using Express framework
    res.status(getErrorHTTPCode(error))
function getErrorHTTPCode(error)
{
    if(error.errorCode == commonErrors.InvalidInput)
        return 400;
    else if...
}

Vous pouvez autre meilleures pratiques liées ici

0
Yonatan

Deux choses m'ont vraiment aidé à résoudre ce problème dans mon code.

  1. Le module 'longjohn', qui vous permet de voir la trace complète de la pile (à travers plusieurs rappels asynchrones).
  2. Une technique de fermeture simple pour conserver les exceptions dans l'idiome standard callback(err, data) (présenté ici dans CoffeeScript).

    ferry_errors = (callback, f) ->
      return (a...) ->
        try f(a...)
        catch err
          callback(err)
    

Maintenant, vous pouvez encapsuler du code non sécurisé et vos rappels traitent tous les erreurs de la même manière: en vérifiant l'argument d'erreur.

0
Jesse Dailey

J'ai récemment créé une simple abstraction appelée WaitFor pour appeler des fonctions asynchrones en mode de synchronisation (basé sur Fibres): https://github.com/luciotato/waitfor

C'est trop nouveau pour être "solide comme un roc". 

en utilisant wait.for, vous pouvez utiliser une fonction asynchrone comme si elles étaient synchronisées, sans bloquer la boucle d'événement du noeud. C'est presque pareil tu es habitué à:

var wait=require('wait.for');

function handleRequest(request, response) {
      //launch fiber, keep node spinning
      wait.launchFiber(handleinFiber,request, response); 
}

function handleInFiber(request, response) {
  try {
    if (request.url=="whatever")
      handleWhateverRequest(request, response);
    else
      throw new Error("404 not found");

  } catch (e) {
    response.writeHead(500, {'Content-Type': 'text/plain'});
    response.end("Server error: "+e.message);
  }
}

function handleWhateverRequest(request, response, callback) {
  if (something) 
    throw new Error("something bad happened");
  Response.end("OK");
}

Puisque vous êtes dans une fibre, vous pouvez programmer séquentiellement le "blocage de la fibre", mais pas la boucle d'événement du noeud.

L'autre exemple:

var sys    = require('sys'),
    fs     = require('fs'),
    wait   = require('wait.for');

require("http").createServer( function(req,res){
      wait.launchFiber(handleRequest,req,res) //handle in a fiber
  ).listen(8124);

function handleRequest(request, response) {
  try {
    var fd=wait.for(fs.open,"/proc/cpuinfo", "r");
    console.log("File open.");
    var buffer = new require('buffer').Buffer(10);

    var bytesRead=wait.for(fs.read,fd, buffer, 0, 10, null);

    buffer.dontTryThisAtHome();  // causes exception

    response.end(buffer);
  }
  catch(err) {
    response.end('ERROR: '+err.message);
  }

}

Comme vous pouvez le constater, j’ai utilisé wait.for pour appeler les fonctions asynchrones du nœud en mode synchro, Sans rappel (visible), afin que tout le code se trouve dans un bloc try-catch.

wait.for lève une exception si l'une des fonctions asynchrones renvoie err! == null

plus d'infos sur https://github.com/luciotato/waitfor

0
Lucio M. Tato