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

http: emit ERR_STREAM_DESTROYED if the socket is destroyed #29227

Closed
wants to merge 3 commits into from

Conversation

@mcollina
Copy link
Member

@mcollina mcollina commented Aug 20, 2019

The pipeline utility assumes that a stream is going to error if it
could not be written to it. If an http.OutgoingMessage was piped after
the underlining socket had been destroyed, the whole pipeline would not
be teared down, resulting in a file descriptor and memory leak.

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • commit message follows commit guidelines
@mcollina
Copy link
Member Author

@mcollina mcollina commented Aug 20, 2019

});

req.on('response', common.mustNotCall());
req.on('error', common.mustCall());

This comment has been minimized.

@BridgeAR

BridgeAR Aug 20, 2019
Member

If I am not mistaken, this results in the ERR_STREAM_DESTROYED as well?

Suggested change
req.on('error', common.mustCall());
req.on('error', common.expectsError({
code: 'ERR_STREAM_DESTROYED'
}));

This comment has been minimized.

@Trott

Trott Aug 23, 2019
Member

ping @mcollina Does that suggestion above seem good to you? (Seems good to me....)

This comment has been minimized.

@mcollina

mcollina Aug 23, 2019
Author Member

That's going to be 'ECONNRESET' on my machine.

lib/_http_outgoing.js Outdated
callback(err);
}
if (this.listenerCount('error') > 0 && this[kErrored] === undefined) {
this[kErrored] = true;

This comment has been minimized.

@BridgeAR

BridgeAR Aug 20, 2019
Member

Is there a specific reason to silence all errors besides the first one?

This comment has been minimized.

@ronag

ronag Aug 20, 2019
Member

@BridgeAR: that's how streams do it, see destroy.js.

This comment has been minimized.

@mcollina

mcollina Aug 20, 2019
Author Member

The reason is that this will normally be called 4 times in case this condition is met, as a response is composed of 4 chunks.

Copy link
Member

@ronag ronag left a comment

Should probably also apply the kErrored logic got writeAfterEndNT, and maybe _implicitHeader and pipe. Maybe refactored out into a helper along the lines of errorOrDestroy?

lib/_http_outgoing.js Outdated
if (callback) {
callback(err);
}
if (this.listenerCount('error') > 0 && this[kErrored] === undefined) {

This comment has been minimized.

@ronag

ronag Aug 20, 2019
Member

What's the reason for the listener count check? We don't do this for streams?

This comment has been minimized.

@ronag

ronag Aug 20, 2019
Member

nit, !this[kErrored]

This comment has been minimized.

@mcollina

mcollina Aug 20, 2019
Author Member

The reason is not to break a lot of tests, and keep this semver-minor.

This comment has been minimized.

@ronag

ronag Aug 20, 2019
Member

Could you add a comment for that at least? Then maybe we can "fix" it in a semver-major in the future?

lib/_http_outgoing.js Outdated
@@ -56,6 +57,7 @@ const HIGH_WATER_MARK = getDefaultHighWaterMark();
const { CRLF, debug } = common;

const kIsCorked = Symbol('isCorked');
const kErrored = Symbol('errored');

This comment has been minimized.

@ronag

ronag Aug 20, 2019
Member

nit kErrorEmitted to more closely follow streams

This comment has been minimized.

@ronag

ronag Aug 20, 2019
Member

Should we initialize kErrored in the constructor? Or will that unnecessarily increase object size?

This comment has been minimized.

@mcollina

mcollina Aug 20, 2019
Author Member

I'm not adding that to the constructor exactly because of that. I can add it if we think it's worth adding, it's not a big deal anyway.

This comment has been minimized.

@ronag

ronag Aug 20, 2019
Member

Sounds good either way to me. Just checking if it was intentional.

lib/_http_outgoing.js Outdated
}
if (this.listenerCount('error') > 0 && this[kErrored] === undefined) {
this[kErrored] = true;
this.emit('error', err);

This comment has been minimized.

@ronag

ronag Aug 20, 2019
Member

I believe this should be emitted in next tick? That's what we want in stream?

This comment has been minimized.

@ronag

ronag Aug 20, 2019
Member

Maybe same thing with callback?

This comment has been minimized.

@mcollina

mcollina Aug 20, 2019
Author Member

I'm actually removing calling the callback here. It's not needed to fix the actual issue, and it can be done in a later step.

This comment has been minimized.

@mcollina

mcollina Aug 20, 2019
Author Member

We cannot remove the callback or something else is going to fail. Essentially if we want to emit error here, we need to call the callback synchronously first :/.

This comment has been minimized.

@ronag

ronag Aug 20, 2019
Member

Something else, as in a test? Add another comment for future semver-major?

@mcollina
Copy link
Member Author

@mcollina mcollina commented Aug 20, 2019

Should probably also apply the kErrored logic got writeAfterEndNT, and maybe _implicitHeader and pipe. Maybe refactored out into a helper along the lines of errorOrDestroy?

I'm not sure about that. The reason I added kErrored was because otherwise we would have 4 'error' events instead of 1 for normal operations.

@mcollina mcollina force-pushed the mcollina:http-outgoing-destroyed branch Aug 20, 2019
@ronag
Copy link
Member

@ronag ronag commented Aug 20, 2019

I'm not sure about that.

Could you explain a little further? In Writable we only emit one error? The only reason (as far as I understand) that this is not a Writable is performance concerns. So in my opinion this should behave as similar as possible without affecting performance.

@mcollina
Copy link
Member Author

@mcollina mcollina commented Aug 20, 2019

The overall reason for not doing that in this PR is to keep it as strictly semver-minor as possible, so it can be backported.

@ronag
Copy link
Member

@ronag ronag commented Aug 20, 2019

The overall reason for not doing that in this PR is to keep it as strictly semver-minor as possible, so it can be backported.

Sounds reasonable. I'm willing to make a semver-major PR in the future to make it more stream-like.

@ronag
ronag approved these changes Aug 20, 2019
lib/_http_outgoing.js Show resolved Hide resolved
@Trott Trott force-pushed the mcollina:http-outgoing-destroyed branch Aug 23, 2019
@Trott
Copy link
Member

@Trott Trott commented Aug 23, 2019

Rebased and force-pushed to get rid of conflict.

@mcollina mcollina force-pushed the mcollina:http-outgoing-destroyed branch 2 times, most recently Aug 23, 2019
@ronag
ronag approved these changes Aug 23, 2019
@mcollina mcollina force-pushed the mcollina:http-outgoing-destroyed branch Aug 23, 2019
@mcollina
Copy link
Member Author

@mcollina mcollina commented Aug 23, 2019

CI is not flaky, windows is failing :/

Mismatched <anonymous> function calls. Expected exactly 2, actual 1.
    at Object.mustCall (c:\workspace\node-test-binary-windows-2\test\common\index.js:337:10)
    at Server.<anonymous> (c:\workspace\node-test-binary-windows-2\test\parallel\test-http-destroyed-socket-write2.js:52:26)
    at Object.onceWrapper (events.js:297:20)
    at Server.emit (events.js:209:13)
    at emitListeningNT (net.js:1262:10)
    at processTicksAndRejections (internal/process/task_queues.js:77:11)
@BridgeAR BridgeAR force-pushed the nodejs:master branch 2 times, most recently from 8ae28ff to 2935f72 May 31, 2020
@jasnell
Copy link
Member

@jasnell jasnell commented Jul 7, 2020

Ping @mcollina and @ronag

@jasnell jasnell added the stalled label Jul 7, 2020
@mcollina
Copy link
Member Author

@mcollina mcollina commented Jul 8, 2020

@ronag if you would like to take this over please do.

@mcollina mcollina requested a review from nodejs/net as a code owner Aug 10, 2020
@aduh95 aduh95 added stalled and removed stalled labels Oct 19, 2020
@github-actions
Copy link

@github-actions github-actions bot commented Oct 19, 2020

This issue/PR was marked as stalled, it will be automatically closed in 30 days. If it should remain open, please leave a comment explaining why it should remain open.

@mcollina
Copy link
Member Author

@mcollina mcollina commented Oct 23, 2020

@ronag wdyt? Should we revive this or should I close it?

@ronag
Copy link
Member

@ronag ronag commented Oct 23, 2020

@ronag wdyt? Should we revive this or should I close it?

@mcollina I'm on vacation next month so I hope to cleanup some of these. Thanks for the ping!

@ronag ronag self-assigned this Dec 27, 2020
@ronag
Copy link
Member

@ronag ronag commented Dec 29, 2020

@mcollina: I think this PR is no longer making correct assumptions. Streams do not emit ERR_STREAM_DESTROYED (at least not anymore) and will only provide it as an error to the callback.

If the underlying socket is prematurely destroyed (without error) that should already error the OutgoingMessage. Though, I'm not sure whether that is actually the case for ServerResponse?

What we should probably fix is:

  • Make sure the callback is invoked with ERR_STREAM_DESTROYED.
  • Have pipeline check during invocation whether any of the streams already is .destroyed and error the pipeline with ERR_STREAM_DESTROYED.

I would recommend we close this PR and make an issue for the callback and pipeline issue. Thoughts?

@ronag ronag removed their assignment Dec 29, 2020
@mcollina
Copy link
Member Author

@mcollina mcollina commented Dec 29, 2020

I'll close this. Could you open the issue?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

8 participants