Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

http2: add missing error handlers #19986

Closed
wants to merge 2 commits into from

Conversation

ryzokuken
Copy link
Contributor

Attach simple error handlers in http2/core.js on sockets where they
seem to be missing to avoid the process from crashing.

Fixes: #19978

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • commit message follows commit guidelines

cc @mafintosh @nodejs/http2

Attach simple error handlers in http2/core.js on sockets where they
seem to be missing to avoid the process from crashing.

Fixes: nodejs#19978
@nodejs-github-bot nodejs-github-bot added dont-land-on-v4.x http2 Issues or PRs related to the http2 subsystem. labels Apr 12, 2018
@@ -868,6 +868,7 @@ class Http2Session extends EventEmitter {

if (!socket._handle || !socket._handle._externalStream) {
socket = new StreamWrap(socket);
socket.on('error', () => {});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should be swallowing the error on these. Those should be forwarded on so they can be properly handled.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jasnell agreed. Should I throw something descriptive inside the function block? Just forwarding the same error further seems counterintuitive.

Perhaps something that'd make the failure (and why exactly it happened) more obvious?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mafintosh and @mcollina ... what do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the error should be forwarded to session.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mcollina

by session, I suppose you mean the current instance of Http2Session itself, right? I inferred that from socket[kSession] = this

If that's the case, I hope the following code does what you expected:

if (!socket._handle || !socket._handle._externalStream) {
       socket = new StreamWrap(socket);
-      socket.on('error', () => {});
+      socket.on('error', (err) => {
+        this.emit('error', err);
+      });
     }

Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a couple of unit tests for this behavior?

@@ -2526,6 +2527,8 @@ function connectionListener(socket) {
'If this is a HTTP request: The server was not ' +
'configured with the `allowHTTP1` option or a ' +
'listener for the `unknownProtocol` event.\n');

socket.on('error', () => {});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you move this to before the call to end()?

@@ -868,6 +868,7 @@ class Http2Session extends EventEmitter {

if (!socket._handle || !socket._handle._externalStream) {
socket = new StreamWrap(socket);
socket.on('error', () => {});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the error should be forwarded to session.

@@ -2526,6 +2527,8 @@ function connectionListener(socket) {
'If this is a HTTP request: The server was not ' +
'configured with the `allowHTTP1` option or a ' +
'listener for the `unknownProtocol` event.\n');

socket.on('error', () => {});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition to the other feedback, could we abstract this into const noop = () => {}; somewhere at the top level? Perhaps close to the emit helper.

@ryzokuken
Copy link
Contributor Author

ryzokuken commented Apr 14, 2018

@apapirovski @mcollina I hope this looks good?

@mcollina I will be adding the required tests by tonight.

@ryzokuken
Copy link
Contributor Author

ryzokuken commented Apr 14, 2018

@mcollina lib/http2.js contains:

'use strict';

process.emitWarning(
  'The http2 module is an experimental API.',
  'ExperimentalWarning', undefined,
  'See https://github.com/nodejs/http2'
);

const {
  constants,
  getDefaultSettings,
  getPackedSettings,
  getUnpackedSettings,
  createServer,
  createSecureServer,
  connect,
  Http2ServerRequest,
  Http2ServerResponse
} = require('internal/http2/core');

module.exports = {
  constants,
  getDefaultSettings,
  getPackedSettings,
  getUnpackedSettings,
  createServer,
  createSecureServer,
  connect,
  Http2ServerResponse,
  Http2ServerRequest
};

My point here: it neither exports Http2Session nor Http2Stream. How am I supposed to test them?

Should I export the two, or should I try to trigger the above failures and errors using the currently exported createServer and the likes?

@ryzokuken
Copy link
Contributor Author

ryzokuken commented Apr 14, 2018

Also, in core.js, it is evident that the constructor for Http2Session takes three arguments:

...
class Http2Session extends EventEmitter {
  constructor(type, options, socket) {
    super();
...

However, in the docs

### Class: Http2Session
not only are the constructor parameters not mentioned anywhere, it would be really helpful if we listed out the valid values a variable like type can hold.

Perhaps I could look for more stuff than just missing event arguments as a part of nodejs/help#877?

Copy link
Member

@apapirovski apapirovski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is on its way but I don't think the current behaviour is quite correct.

@@ -868,6 +870,9 @@ class Http2Session extends EventEmitter {

if (!socket._handle || !socket._handle._externalStream) {
socket = new StreamWrap(socket);
socket.on('error', (err) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what the proper error handling would look like here. I think it might need to do more than emit and it should also be aware of whether the socketOnError already fired previously.

Maybe something like this would be more accurate:

const wrappedSocket = socket;
socket = new StreamWrap(wrappedSocket);
socket.on('error', socketOnError.bind(wrappedSocket));

This would avoid the JSStreamWrap propagating an already handled error from the underlying socket and emitting on the session twice.

/cc @jasnell @mcollina @addaleax

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think this might need to handle socket.on('close') in a similar manner. So something like:

socket.on('close', socketOnClose.bind(wrappedSocket));

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Proper error handling here is a simple .on('error', () => {}) as the underlying stream is already handled before hitting this switch

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

@apapirovski apapirovski Apr 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Proper error handling here is a simple .on('error', () => {}) as the underlying stream is already handled before hitting this switch

That presumes that the wrap can't error itself...

(Which it can for example on line 56 in the JSStreamWrap itself, not to mention the fact that it inherits from Socket so there are likely other errors possible too.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like input from @mcollina and @jasnell if possible. I think the following is correct:

const wrappedSocket = socket;
socket = new StreamWrap(wrappedSocket);
socket.on('error', socketOnError.bind(wrappedSocket));
socket.on('close', socketOnClose.bind(wrappedSocket));

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that will trigger the error twice, as the wrappedSocket already triggers socketOnClose and socketOnError.

I think it would be fine to trigger an error if the wrappedSocket is in objectMode immediately as that seems to be the only case where the streamwrap emits a non-forwarded error? And then socket.on('error', () => {}) the rest, as they are already handled

Copy link
Member

@mafintosh mafintosh Apr 19, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the http2 code it seems ok to call socketOnError more than once as that simply calls destroy which should guard against that. The onClose method has a bit more going on so can't tell if that is safe to do there, but i don't think we need to forward that in either case?

so

const wrappedSocket = socket;
socket = new StreamWrap(wrappedSocket);
socket.on('error', socketOnError.bind(wrappedSocket));

?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that will trigger the error twice, as the wrappedSocket already triggers socketOnClose and socketOnError.

It will just bail because socketOnError removes the session assignment in a sync call. I'm also not sure that the error in objectMode is the only one. The wrap is a socket in itself, it can throw other related errors.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And I think you're probably right re: leaving out close listener. So the thing posted above seems good to me.

@apapirovski
Copy link
Member

ping @addaleax re: best ways to test the error handling on JSStreamWrap. Any ideas? (In ways that don't trigger the error on the underlying socket instead.)

@apapirovski
Copy link
Member

apapirovski commented Apr 16, 2018

not only are the constructor parameters not mentioned anywhere, it would be really helpful if we listed out the valid values a variable like type can hold.

If it's not a part of the public API so we shouldn't really expose that type of information since it wouldn't be of any use and could be deceptive regarding its usage.

Should I export the two, or should I try to trigger the above failures and errors using the currently exported createServer and the likes?

For the unknown protocol one, we could create a straightforward server, try to connect using http1 (via https) and do something like:

server.on('connection', (socket) => {
  socket.emit('error', new Error('connection error'));
});

then that either blows it up or doesn't.

@mafintosh
Copy link
Member

To test the streamwrap make a test that uses the createConnection api to return a stream that fails on the first write

@ryzokuken
Copy link
Contributor Author

ping @addaleax

@BridgeAR
Copy link
Member

@nodejs/http2 @nodejs/streams @addaleax PTAL

@mcollina
Copy link
Member

This is waiting for a unit test to be written, see #19986 (comment) as a suggestion.

@ryzokuken
Copy link
Contributor Author

@mcollina sorry that I totally forgot about this, will start working on the test.

@jasnell
Copy link
Member

jasnell commented Aug 10, 2018

Ping @ryzokuken

@ryzokuken
Copy link
Contributor Author

@jasnell I have lost a lot of context on this, but IIRC, there were a few disagreements regarding the nature of tests. I'll hit up @mafintosh over the course of the next week and try to get this one in, thanks!

@jasnell
Copy link
Member

jasnell commented Sep 10, 2018

Ping again :-)

@jasnell jasnell added the stalled Issues and PRs that are stalled. label Sep 10, 2018
@antsmartian
Copy link
Contributor

Ping pong @ryzokuken :)

@BridgeAR
Copy link
Member

BridgeAR commented Mar 9, 2019

Closing due to a very long inactivity. @ryzokuken please reopen or open a new PR in case you want to continue working on this.

@BridgeAR BridgeAR closed this Mar 9, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
http2 Issues or PRs related to the http2 subsystem. stalled Issues and PRs that are stalled.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Missing http2 error handling
9 participants