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: OutgoingMessage change writable after end #14024

Conversation

Kasher
Copy link
Contributor

@Kasher Kasher commented Jul 1, 2017

When an OutgoingMessage is closed (for example, using the end
method), its 'writable' property should be changed to false - since it
is not writable anymore. The 'writable' property should have the
opposite value of the 'finished' property.

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • commit message follows commit guidelines
Affected core subsystem(s)

http

Issue: #14023

@nodejs-github-bot nodejs-github-bot added the http Issues or PRs related to the http subsystem. label Jul 1, 2017
@gireeshpunathil
Copy link
Member

Thank you! Does it make sense to add another test case which asserts that response refuses to write after end()? While your test is fine, it looks to me more directed, rather than testing the function of Outgoing Message.

Here is a code which writes after end() with the existing code - you may tune this into a test case:

var http = require('http')
const net = require('net')

var server = http.createServer(function(req, res) {
  res.write('hello ')
  res.end('write after ')
  res._send('end!')
})
server.listen(25000, () => {
  var data = 'GET / HTTP/1.0\r\nContent-Type: text/plain\r\nContent-Length: 0\r\n\r\n'
  const client = net.createConnection({ port: 25000}, () => {
    client.write(data)
  })
  client.on('data', (data) => {
    console.log(data.toString())
  })
  client.on('end', () => {
    server.close()
  })
})

@Kasher
Copy link
Contributor Author

Kasher commented Jul 2, 2017

@gireeshpunathil , Thanks for your response.

I totally agree with what you wrote (my test is quite too strict to the case I fixed), but I wasn't able to write a less-strict test that reproduces this issue.

Unfortunately, the code you attached doesn't reproduce the issue.
The issue I'm referring to is an edge case, which happens due to some race between the piping code and the fact that the response was closed. I attached some code that reproduces this to the issue I opened (#14023), but even with that code - it doesn't reproduce every time :\

When I tried to execute the code you wrote, I got the following result:

HTTP/1.1 200 OK
Date: Sun, 02 Jul 2017 16:27:59 GMT
Connection: close

hello write after end!

Which is fine in my opinion.

Any call to _send or write in OutgoingMessage after the response was closed will result in a "write after end" error, but that's by design. I believe that that's not the case when you use streams though (where you don't directly call send or write or any method on the response).

Any suggestion on how to reproduce this consistently will be much appreciated. I would be more than happy to write a less-strict test that reproduces this :)

Thanks again!

@gireeshpunathil
Copy link
Member

@Kasher - does this fail for you? (where data is a huge file in the disc)

const http = require('http')
const fs = require('fs')

const server = http.createServer((req, res) => {
  req.on('end', () => {
    console.log("Server: Client request was closed, closing server's request and client's response.");
    res.end()
  })
  fs.createReadStream('data').pipe(res)
})
server.listen(25000, () => {
  const client = http.get('http://localhost:25000/', (res) => {
    res.pipe(process.stdout)
  })
})

@Kasher Kasher force-pushed the http_outgoing_mark_as_not_writable_upon_close branch 2 times, most recently from 641175f to 554d987 Compare July 7, 2017 12:38
@Kasher
Copy link
Contributor Author

Kasher commented Jul 7, 2017

@gireeshpunathil Thanks again.
Unfortunately, this doesn't reproduce this either.
I've managed to reproduce it in a small unit test, and I added it to my PR: https://github.com/nodejs/node/pull/14024/files#diff-f48f06a77b4005be724d14b42ab7f87d

@Kasher Kasher force-pushed the http_outgoing_mark_as_not_writable_upon_close branch from 554d987 to 28b4f4d Compare July 7, 2017 12:53
When an OutgoingMessage is closed (for example, using the `end`
method), its 'writable' property should be changed to false - since it
is not writable anymore. The 'writable' property should have the
opposite value of the 'finished' property.
@Kasher Kasher force-pushed the http_outgoing_mark_as_not_writable_upon_close branch from 28b4f4d to 20b6326 Compare July 7, 2017 12:59
@gireeshpunathil
Copy link
Member

thank you!

@Kasher
Copy link
Contributor Author

Kasher commented Jul 8, 2017

Thanks for approving this @gireeshpunathil .
Just asking since I'm not sure - how do we proceed? Should we open a CI?
Thanks in advance!

@gireeshpunathil
Copy link
Member

@Kasher - thanks for checking. Let us wait for one or two more reviews.
/cc @nodejs/http @nodejs/streams
CI: https://ci.nodejs.org/job/node-test-pull-request/9049/

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.

Good spot. I think this should be fixed, but I also think that OutgoingMessage.pipe() should throw an exception.

port: server.address().port,
method: 'GET',
path: '/'
}).end();
Copy link
Member

Choose a reason for hiding this comment

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

What http.request  returns is an OutgoingMessage as well, we might want to change that it there as well.


// If we got here - 'write after end' wasn't raised and the test passed.
setTimeout(() => server.close(), 10);
}, 10);
Copy link
Member

Choose a reason for hiding this comment

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

This is going to be very flaky. Can you refactor the test to not rely on timers?

Copy link
Contributor Author

@Kasher Kasher Jul 10, 2017

Choose a reason for hiding this comment

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

Sure thing. I totally agree with you. Basically I needed setTimeout with a timeout of zero (or process.nextTick). I'll change to one of these.

const server = http.createServer(common.mustCall(function(req, res) {
console.log('Got a request, piping an OutgoingMessage to it.');
const outgointMessage = new OutgoingMessage();
outgointMessage.pipe(res);
Copy link
Member

Choose a reason for hiding this comment

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

OutgoingMessage should really be write-only, and not be piped. The fact that OutgoingMessage has a pipe  method is related to Node.js history, and it should not be used. Why Does this use case matter to you?

Copy link
Contributor Author

@Kasher Kasher Jul 10, 2017

Choose a reason for hiding this comment

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

I agree. OutgoingMessage is just an example of a type that inherits from Stream. A better practice would be to create a new type that inherits from Stream and use this in the test. I would change this.

I chose OutgoingMessage since if I pipe from it, the outcome will be very similar to what happens when I use request npm .

I guess this should either be disabled (by throwing an exception as you suggested), or it should work.
Here is what they do in request npm:

  1. They call to pipe on a type that inherits from Stream (the type is called Request):
    https://github.com/request/request/blob/master/request.js#L1489
  2. Upon data on an IncomingResponse, they emit this on the Request:
    https://github.com/request/request/blob/master/request.js#L1082

For more detailed explanation you can read my reply: #14024 (comment)

@Kasher
Copy link
Contributor Author

Kasher commented Jul 10, 2017

@mcollina, Thank you very much for you review.
I totally agree with your comments.
The write after end error is raised when I use request npm and when I call their implementation of pipe. Lets go over this:
First, I guess request npm implemented pipe as they did to support something like that:

req.pipe(request('http://mysite.com/doodle.png')).pipe(resp)

(taken from their main page).

We can see from their source code, that when I call pipe on a Request (which is a type that inherits from Stream: https://github.com/request/request/blob/master/request.js#L131 ), they basically call stream's pipe: https://github.com/request/request/blob/master/request.js#L1489 .

Then, they create a ClientRequest: https://github.com/request/request/blob/master/request.js#L748 .
When the response event is raised, they call a method called onRequestResponse: https://github.com/request/request/blob/master/request.js#L772

In this method, we can see that they register to the reponse's data event, and they emit it on the Request (the type that inherits from Stream and was originally piped): https://github.com/request/request/blob/master/request.js#L1082

This might raise the write after end error, and this is what I try to avoid.

I will try to refactor my unit tests so they won't use OutgoingMessage. In other words, I will reproduce such behavior with a custom type that inherits from Stream, instead of abusing OutgoingMessage.

However, I guess their current implementation shouldn't cause the write after end flow, since this if clause: https://github.com/nodejs/node/blob/master/lib/internal/streams/legacy.js#L15 should return false (which doesn't happen now, and that's what my PR fixes).

Should I throw an exception in OutgoingMessage's pipe method as well (as it should be write-only and shouldn't be piped)? It won't solve the issue, but it might save some time to other developers that try to pipe from OutgoingMessage for some reason.

Thanks again, waiting for your reply for further instructions :)

@mcollina
Copy link
Member

@Kasher looking at request code, the problem you are facing does not come down from OutgoingMessage, but from the Request  object (https://github.com/request/request/blob/master/request.js#L118), as they are not setting writable = false  either.

@Kasher
Copy link
Contributor Author

Kasher commented Jul 10, 2017

@mcollina Thanks, but I'm not sure I fully understand what you mean. I pipe a Request object to a ServerResponse object (in order to pipe something from the internet back to the client). ServerResponse inherits from OutgoingMessage: https://github.com/nodejs/node/blob/master/lib/_http_server.js#L122 .
The code here: https://github.com/nodejs/node/blob/master/lib/internal/streams/legacy.js#L15 checks if the destination is writable, and if I understand this correctly - the destination is an instance of ServerResponse (which means it is also an instance of OutgoingMessage).

Did I miss something?

Thanks again :)

@mcollina
Copy link
Member

I think your request example is calling Response.prototype.pipe, not OutgoingMessage.prototype.pipe.

@Kasher
Copy link
Contributor Author

Kasher commented Jul 10, 2017

@mcollina Thanks again, but I'm not sure I get it. I use serverRequest.pipe(res); in my example, where serverRequest is an instance of Request. So basically I call Request.prototype.pipe, which calls to Stream.prototype.pipe:

In this call, self is my serverRequest (an instance of Request), and dest is res (an instance of ServerResponse).

Later on, when data is emitted on serverRequest, we reach this code: https://github.com/nodejs/node/blob/master/lib/internal/streams/legacy.js#L15
and over here, again, dest is my res, which is a ServerResponse, which inherits from OutgoingMessage. So the call to write is executed on an OutgoingMessage (in addition, the fact that we see

Error: write after end
    at ServerResponse.OutgoingMessage.write (_http_outgoing.js:442:15)at  ServerResponse.OutgoingMessage.write (_http_outgoing.js:442:15) 

in the stack supports this). In addition, when I added the fix, the error is never thrown (in other words, I couldn't reproduce this issue).

What did I miss?

Thanks.

@mcollina
Copy link
Member

@Kasher in this line https://github.com/nodejs/node/pull/14024/files#diff-f48f06a77b4005be724d14b42ab7f87dR9, you are testing the reverse of that pipe.

@Kasher
Copy link
Contributor Author

Kasher commented Jul 10, 2017

@mcollina Oh, you're talking about the test!
Sorry, I totally misunderstood you. I was talking about this issue I opened: #14023

Regarding the test - You're right. OutgoingMessage is just an example of a type that inherits from Stream. It was simple and quick for me to try and reproduce it using this type. A better practice would be to create a new type that inherits from Stream and to use this in the test. I will change this.

Should I add throw an exception in OutgoingMessage's pipe function? This would have saved us several hours :)

Thanks again, and sorry for the inconvenience.

@mcollina
Copy link
Member

Let's separate the .pipe() change to the .writable = false change as the first would be semver-major.

@Kasher
Copy link
Contributor Author

Kasher commented Jul 11, 2017

@mcollina , I've made the required changes.
I hope the tests are better now. I would really appreciate it if you could take a look.

Thanks!

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.

There a bit more nits on the test, but this looks good.

LGTM when those are solved.

assert(res.writable, 'Res should be writable when it is received \
and opened.');
assert(!res.finished, 'Res shouldn\'t be finished when it is received \
and opened.');
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 use assert.strictEqual without a message here and in the rest of the tests?

const common = require('../common');
const assert = require('assert');
const http = require('http');

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 add a little comment explaining what this test is about here?

myStream.pipe(res);

process.nextTick(() => {
console.log('Closing response.');
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 remove the console.log statements?


// If we got here - 'write after end' wasn't raised and the test passed.
process.nextTick(() => server.close());
});
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 also add common.mustCall on both the process.nextTick.

@Kasher
Copy link
Contributor Author

Kasher commented Jul 11, 2017

@mcollina done :)

const http = require('http');
const util = require('util');
const stream = require('stream');

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 add a little explanation here as well?

@mcollina
Copy link
Member

This is ok to be backported to 6.x when the time is due.

@Kasher
Copy link
Contributor Author

Kasher commented Jul 30, 2017

Thank you very much @mcollina @TimothyGu !
Much appreciated :)

@MylesBorins
Copy link
Contributor

@mcollina what about v4.x? would you consider this a major bug?

MylesBorins pushed a commit that referenced this pull request Aug 16, 2017
When an OutgoingMessage is closed (for example, using the `end`
method), its 'writable' property should be changed to false - since it
is not writable anymore. The 'writable' property should have the
opposite value of the 'finished' property.

PR-URL: #14024
Fixes: #14023
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Gireesh Punathil <gpunathi@in.ibm.com>
@mcollina
Copy link
Member

I'm ok with backporting to 6.x and 4.x.

@MylesBorins MylesBorins mentioned this pull request Aug 16, 2017
MylesBorins pushed a commit that referenced this pull request Aug 16, 2017
When an OutgoingMessage is closed (for example, using the `end`
method), its 'writable' property should be changed to false - since it
is not writable anymore. The 'writable' property should have the
opposite value of the 'finished' property.

PR-URL: #14024
Fixes: #14023
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Gireesh Punathil <gpunathi@in.ibm.com>
@mcollina
Copy link
Member

Unfortunately we need to revert this: #15404.

mcollina added a commit to mcollina/node that referenced this pull request Sep 20, 2017
Setting writable = false in IncomingMessage.end made some errors
being swallowed in some very popular OSS libraries that we must
support. This commit add some of those use cases to the tests,
so we avoid further regressions.
We should reevaluate how to set writable = false in IncomingMessage
in a way that does not break the ecosystem.

See: nodejs#14024
Fixes: nodejs#15029
mcollina added a commit that referenced this pull request Sep 20, 2017
Setting writable = false in IncomingMessage.end made some errors
being swallowed in some very popular OSS libraries that we must
support. This commit add some of those use cases to the tests,
so we avoid further regressions.
We should reevaluate how to set writable = false in IncomingMessage
in a way that does not break the ecosystem.

See: #14024
Fixes: #15029
PR-URL: #15404
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
jasnell pushed a commit that referenced this pull request Sep 20, 2017
Setting writable = false in IncomingMessage.end made some errors
being swallowed in some very popular OSS libraries that we must
support. This commit add some of those use cases to the tests,
so we avoid further regressions.
We should reevaluate how to set writable = false in IncomingMessage
in a way that does not break the ecosystem.

See: #14024
Fixes: #15029
PR-URL: #15404
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Qard pushed a commit to Qard/ayo that referenced this pull request Sep 21, 2017
Setting writable = false in IncomingMessage.end made some errors
being swallowed in some very popular OSS libraries that we must
support. This commit add some of those use cases to the tests,
so we avoid further regressions.
We should reevaluate how to set writable = false in IncomingMessage
in a way that does not break the ecosystem.

See: nodejs/node#14024
Fixes: nodejs/node#15029
PR-URL: nodejs/node#15404
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Qard pushed a commit to Qard/ayo that referenced this pull request Sep 21, 2017
Setting writable = false in IncomingMessage.end made some errors
being swallowed in some very popular OSS libraries that we must
support. This commit add some of those use cases to the tests,
so we avoid further regressions.
We should reevaluate how to set writable = false in IncomingMessage
in a way that does not break the ecosystem.

See: nodejs/node#14024
Fixes: nodejs/node#15029
PR-URL: nodejs/node#15404
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
@MylesBorins
Copy link
Contributor

Looks like this never made it to v4.x or 6.x

phew

@mcollina
Copy link
Member

This was close.

@missbruni
Copy link

How should we be solving this issue? Is there another resolution? I am having the same bug when trying to pipe streams.

@ronag ronag mentioned this pull request Jul 16, 2019
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
http Issues or PRs related to the http subsystem.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants