Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.


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
TLS: improve compliance with shutdown standard, remove hacks #36111
TLS: improve compliance with shutdown standard, remove hacks #36111
Changes from all commits
f199b1ca18156cFile filter
Conversations
Jump to
There are no files selected for viewing
ronagNov 15, 2020
Member
Why is this change needed?
vtjnashNov 15, 2020
Author
Contributor
As the comment says, this should be a no-op. The underlying socket is not supposed to deliver a read event after a close event, but TLS would try to do so (while racing to destroy it first). Looking back at the commit history, it was added as a hack to work around this issue, but didn't really fix it, as evidenced by flaky CI.
lpincaJul 30, 2021
•
edited
Member
If there is buffered data (for example if there is buffered data and the socket is destroyed) shouldn't it be read? We do something similar in
wsto ensure that all received data is actually used and delivered to the user.vtjnashJul 30, 2021
Author
Contributor
There is no data buffered here, that wouldn't make any sense as the http protocol doesn't allow for more http data to be included after the http message is ended.
lpincaJul 30, 2021
•
edited
Member
How do you know that the http message ended? Isn't this only a listener for the
'close'event of the socket? That'close'event can be emitted in the middle of a HTTP request/response.vtjnashAug 2, 2021
Author
Contributor
I am not sure what you mean by calling it "only" the 'close' event. The 'close' event is emitted to signal the end of an HTTP request/response stream. Ergo it is logically incompatible for it to happen in the middle.
Ref: https://nodejs.org/api/stream.html#stream_event_close_1 "This event indicates that no more events will be emitted, and no further computation will occur."
lpincaAug 2, 2021
Member
This
'close'event is emitted by the{net,tls}.Socketand it can be emitted when there is buffered data in its readable queue. For example if the socket is paused and it gets destroyed.vtjnashAug 2, 2021
Author
Contributor
The documentation for the 'close' event I linked there forbids calling the
ondatacallback here, or any other future callbacks, so the comment there must be wrong about that expectation. Note that the previous bug this was supposed to address wasn't even pulling through a final buffered chunk, but instead was tricking nodejs into thinking there was an error handler for events arriving in the next event loop tick, which mostly hides the underlying bug when doing local testing on a loop-back interface on the linux kernel, so this comment was also incorrect in its description of why it is added.If you got here by calling 'destroy', then calling
.read()here afterwards introduces a multi-process data race, and no number of.read()calls is sufficient to recover from that error.lpincaAug 2, 2021
Member
@vtjnash this is basically what I mean:
without that
socket.read()after the'close'event that data that was actually received might never be used.vtjnashAug 2, 2021
Author
Contributor
I am not sure what your example is supposed to show, as you are using the return value of
socket.readto "prove" that a callback happens elsewhere which will not happen. If I modify your example to match the description of behavior in the comment, we can see that the behavior asserted in the comment is never what actually happens.This logs "close", and then will never call "data" (because the stream is paused). If the comment was correct, it should then also print "data 16384" afterwards.
vtjnashAug 2, 2021
•
edited
Author
Contributor
Eww, okay, I dropped the
socket.read()call, and then that printed "data 16384", in contradiction to the documentation which states that "no more events will be emitted, and no further computation will occur.But back to the actual context here: why would http ever be calling
.pauseand.destroyon the underlying socket in the middle of reading a message?lpincaAug 2, 2021
•
edited
Member
@vtjnash
I'm only calling
socket.pause()for simplicity here. It could be done implicitly for backpressure handling when the HTTP response is piped to a slower stream. Withoutsocket.read()the'data'event is not emitted and the data is simply lost. Again, we use the same pattern inws.Because the HTTP request and response are streams that can be piped around and as streams they might be destroyed.
vtjnashAug 2, 2021
Author
Contributor
If you bypass the HTTP protocol to call 'destroy' in the middle of reading the response, isn't it expected that possible unread data to be lost? The documentation for 'destroy' is not entirely clear on what it does precisely to buffered data (just that it should "release any internal resources" and should truncate the stream by ignoring any future data received).
But it sounds like this situation would be bypassing the internal requirements of the http module, to explicitly violate the http protocol. That doesn't sound like something this code should have been attempting to partially handle.
vtjnashAug 2, 2021
Author
Contributor
Looking at 'ws' for '.destroy' calls, most of them happen after 'end', which would be unaffected by this removal (in addition to not being http, so really not relevant to this code also). Or in places where it wants to immediately stop further processing on the stream (e.g. auth failures). The only place there that I see an issue with the use of 'destroy' is in a bit of example code, which I see you merged recently (websockets/ws#1798) which tries to do a write immediate before a destroy call, which the documentation for 'writable.destroy' warns against (see my comment there), but that doesn't seem to be particularly relevant here to our discussion of the handling of 'readable.destroy' either.
lpincaAug 2, 2021
•
edited
Member
I don't know. In
wswe want all received bytes to be parsed, including the ones kept in the socket buffer because they have actually been read and reached the peer/application.If you are confident that this will not cause issues in practice because userland modules are not relying on current behavior, then I'm fine with it.
lpincaAug 2, 2021
•
edited
Member
This change does not affect
wsluckily. I've only mentioned it because the same pattern used here (to read buffered data after'close') is also used bywsas per #36111 (comment).vtjnashAug 2, 2021
Author
Contributor
Did you have an example for why you added those lines to
ws? I see only the commit where you added it:websockets/ws@6046a28. Note that the documentation for 'error' also indicates that this call to
.readshould not trigger any callbacks to occur ("no further events other than 'close' should be emitted"), so by adding that comment and code there, it seems like you may be causingwsto violate thestream.Readableinterface.vtjnashAug 2, 2021
Author
Contributor
It also seems like you had previously changed it to explicitly have the opposite behavior in websockets/ws@5d8ab0e, but was changed back later during a general code refactoring? From the comment description, it appears to me to say that code was intending to be be part of the 'error' handler (before that handler called destroy), and should be triggering the 'readable' callback, not the 'read' callback.
But why would the underlying socket be doing an active 'read', thus causing it to encounter an 'error', while data was already pending in the buffer? That seems like it would be disregarding of flow control, so wouldn't actually happen in a correctly implemented stream.
lpincaAug 3, 2021
•
edited
Member
AFAIK calling
readable.read()is the only way to pull out buffered data from a readable stream after it is destroyed and in my opinion there must be a way to do that. The fact that doing so causes an additional'data'event is a side effect that is actually very useful.See #36111 (comment) and #36111 (comment). The user can call
ws.terminate()and destroy the socket. When that happens there might be buffered data in the socket. That data might contain many vaid WebSocket frames that we do not want to drop.websockets/ws@5d8ab0e is still in place, see https://github.com/websockets/ws/blob/8.0.0/lib/websocket.js#L946. Any data after the close frame must be discarded per specification.
Anyway this is not about
ws.wsis not affected by this change. This is about discarding data that was previously not discarded.ronagAug 3, 2021
Member
I haven't fully followed this discussion but #39639 might be of relevance.
jasnellNov 20, 2020
Member
My concern here is that this change may effect other types of streams that aren't TLS. @ronag @addaleax ... any thoughts here?
ronagNov 20, 2020
•
edited
Member
I’m ok with the change. The eof check logic has basically just been moved into the native parts.
vtjnashNov 20, 2020
Author
Contributor
Yes, this code, added in August, was breaking for other stream types that weren't TLS.