Frage NodeJS: http.ClientRequest löst in einem bestimmten Szenario zweimal ein Fehlerereignis aus


Betrachten Sie folgenden JavaScript-Code, der in nodejs ausgeführt wird:

// create ClientRequest
// port 55555 is not opened
var req = require('http').request('http://localhost:55555', function() {
  console.log('should be never reached');
});

function cb() {
  throw new Error();
}

req.on('error', function(e) {
 console.log(e);
 cb();
});

// exceptions handler
process.on('uncaughtException', function() {
 console.log('exception caught. doing some async clean-up before exit...');
 setTimeout(function() {
   console.log('exiting');
   process.exit(1);
 }, 2000);
});

// send request
req.end();

Erwartete Ausgabe:

{ Error: connect ECONNREFUSED 127.0.0.1:55555
    at Object.exports._errnoException (util.js:1026:11)
    at exports._exceptionWithHostPort (util.js:1049:20)
    at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1081:14)
  code: 'ECONNREFUSED',
  errno: 'ECONNREFUSED',
  syscall: 'connect',
  address: '127.0.0.1',
  port: 55555 }
exception caught. doing some async clean-up before exit...
exiting

Tatsächliche Ausgabe:

{ Error: connect ECONNREFUSED 127.0.0.1:55555
    at Object.exports._errnoException (util.js:1026:11)
    at exports._exceptionWithHostPort (util.js:1049:20)
    at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1081:14)
  code: 'ECONNREFUSED',
  errno: 'ECONNREFUSED',
  syscall: 'connect',
  address: '127.0.0.1',
  port: 55555 }
exception caught. doing some async clean-up before exit...
{ Error: socket hang up
    at createHangUpError (_http_client.js:252:15)
    at Socket.socketCloseListener (_http_client.js:284:23)
    at emitOne (events.js:101:20)
    at Socket.emit (events.js:188:7)
    at TCP._handle.close [as _onclose] (net.js:492:12) code: 'ECONNRESET' }
exception caught. doing some async clean-up before exit...
exiting

Wie Sie sehen können, löst http.ClientRequest (oder vielleicht stream.Writable?) Das Fehlerereignis zweimal aus, zuerst mit ECONNREFUSED und nach der Ausnahme wird ECONNRESET abgefangen.

Dies tritt nicht auf, wenn wir den asynchronen Rückruf im http.ClientRequest-Fehlerhandler mit nextTick oder setTimeout ausführen, z. Diese Änderung ergibt das erwartete Verhalten:

req.on('error', function(e) {
 console.log(e);
 process.nextTick(cb);
});

Kann jemand erklären, warum das passiert und ob das ein Fehler ist oder wie erwartet funktioniert? Das Verhalten ist im letzten Knoten 4.x und Knoten 6.x identisch.

Vielen Dank!


5
2017-08-19 10:08


Ursprung


Antworten:


Zusammenfassung

Das Problem ist, dass Sie einen nicht erfassten Fehler innerhalb des Listener-Callbacks auslösen. Der Knoten behandelt absichtlich keine unerwarteten Exceptions, daher muss der Code, der normalerweise nach diesem Fehler ausgeführt wird, als Steuerelement an den entsprechenden Catch (in diesem Fall den ganzen Weg zur nicht abgefangenen Exception-Behandlung) übergeben werden. Dies verursacht einige Dinge passieren nicht, aber am auffälligsten ist das HTTPRequest ist nicht bewusst, dass es erfolgreich einen Fehler ausgegeben hat, wenn es für das Schließen des Sockets aufgerufen wird.

Details und Referenzen

Die allgemeine Philosophie von Node ist keine unerwarteten Würfe zu fangen und Knoten behandelt dieses Muster als a Programmierfehler das sollte erlaubt sein, einen Misserfolg zu erreichen. (In der Dokumentation wird die API für einen Ereignis-Listener-Callback nicht explizit erläutert, aber es wird auch kein Beispiel gegeben, in dem eine Ausnahme oder Anweisungen zur Berücksichtigung des Emitters auf der Entwicklerseite der Stream-API ausgegeben werden.)

Wenn sich Ihre Ausnahme bis zum Emitter ausbreitet und nachfolgende Listener und der Rest seiner Bereinigung und Markierung des Sockets nicht auftreten, verursacht dies ClientRequest zu denken, dass es wieder einen Fehler geben muss:

Das Emitter Nach dem Rückruf wird ein Code ausgegeben, der den zweiten Fehler unterdrückt:

req.emit('error', err);
// For Safety. Some additional errors might fire later on
// and we need to make sure we don't double-fire the error event.
req.socket._hadError = true;

Da dein Wurf dort nicht gefangen wurde, hat der Prüfen für diese Variable findet dann _hadError ist immer noch unscharf:

if (!req.res && !req.socket._hadError) {
  // If we don't have a response then we know that the socket
  // ended prematurely and we need to emit an error on the request.
  req.emit('error', createHangUpError());
  req.socket._hadError = true;
}

Wenn Sie Ihren Fehler in einen anderen asynchronen Block werfen, verhindern Sie nicht, dass der Rest des Socket-Cleanup-Prozesses fortgesetzt wird, da die Exception in einem anderen Funktionsstapel auftritt.

In einigen anderen Fällen ist Knoten vorsichtig, wenn Callbacks aufgerufen werden und was zuvor festgelegt wurde. Dies wird jedoch weitgehend gemacht, um Callbacks zu erlauben, alternative Aufräumarbeiten durchzuführen, usw., wie dies hier gezeigt wird Kommentar:

we set destroyed to true before firing error callbacks in order
to make it re-entrance safe in case Socket.prototype.destroy()
is called within callbacks

Die Reihenfolge der Einstellung wird vertauscht _hadError und das Ausstrahlen würde diesen zweiten Fehler für Sie unterdrücken und scheint sicher zu sein, da dies der einzige zu sein scheint _hadError prüfen. Aber:

  • Wenn dies verwendet wird, um in der Zukunft mehr Fehler zu unterdrücken, würde dies Fehlerrückrufe beeinträchtigen, die versuchen, den Zustand der Verbindung während eines Fehlers zu untersuchen

  • Es würde immer noch den Sockel in einem teilweise aufgeräumten Zustand verlassen, der für längere lebende Programme nicht schön ist.

Daher würde ich im Allgemeinen sagen, dass es besser ist, Ausnahmen nicht direkt in Rückrufe zu werfen, es sei denn, Sie haben eine ungewöhnliche Situation, in der die normale interne Behandlung verhindert oder außer Kraft gesetzt werden muss.


3
2017-08-27 11:52