-
Notifications
You must be signed in to change notification settings - Fork 30.6k
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: prevent aborted event when request is complete #18999
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
'use strict'; | ||
|
||
const common = require('../common'); | ||
const http = require('http'); | ||
const assert = require('assert'); | ||
const fs = require('fs'); | ||
const Countdown = require('../common/countdown'); | ||
|
||
function cleanup(fname) { | ||
try { | ||
if (fs.statSync(fname)) fs.unlinkSync(fname); | ||
} catch (err) {} | ||
} | ||
|
||
const N = 2; | ||
const fname = '/dev/null'; | ||
let abortRequest = true; | ||
|
||
const server = http.Server(common.mustCall((req, res) => { | ||
const headers = { 'Content-Type': 'text/plain' }; | ||
headers['Content-Length'] = 50; | ||
const socket = res.socket; | ||
res.writeHead(200, headers); | ||
setTimeout(() => res.write('aaaaaaaaaa'), 100); | ||
setTimeout(() => res.write('bbbbbbbbbb'), 200); | ||
setTimeout(() => res.write('cccccccccc'), 300); | ||
setTimeout(() => res.write('dddddddddd'), 400); | ||
if (abortRequest) { | ||
setTimeout(() => socket.destroy(), 600); | ||
} else { | ||
setTimeout(() => res.end('eeeeeeeeee'), 1000); | ||
} | ||
}, N)); | ||
|
||
server.listen(0, common.mustCall(() => { | ||
cleanup(fname); | ||
download(); | ||
})); | ||
|
||
const finishCountdown = new Countdown(N, common.mustCall(() => { | ||
server.close(); | ||
})); | ||
const reqCountdown = new Countdown(N, common.mustCall()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would prefer if It's not clear why you are calling twice too. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's running two tests - one to ensure we don't abort a good request, one to ensure we do abort a bad one. i can refactor to take out the countdown timers altogether. was trying to stay close to how other tests are constructed. if you can suggest how to change i am happy to refactor. |
||
|
||
function download() { | ||
const opts = { | ||
port: server.address().port, | ||
path: '/', | ||
}; | ||
const req = http.get(opts); | ||
req.on('error', common.mustNotCall()); | ||
req.on('response', (res) => { | ||
assert.strictEqual(res.statusCode, 200); | ||
assert.strictEqual(res.headers.connection, 'close'); | ||
let aborted = false; | ||
const fstream = fs.createWriteStream(fname); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this file can be made more simple. const { Writable } = require('stream');
// ...
const writable = new Writable({
write(chunk, encoding, callback) {
callback();
}
});
res.pipe(writable);
// ...
writable.on('finish', () => { |
||
res.pipe(fstream); | ||
const _handle = res.socket._handle; | ||
_handle._close = res.socket._handle.close; | ||
_handle.close = function(callback) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you explain why are you overriding this here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. because i could not find a way of forcing the condition we get intermittently in the wild. if you run this script and leave it it will eventually start aborting requests that are actually complete: https://gist.github.com/billywhizz/b500a0d4708f89625ddbb313601b5363. i was unable to figure out how to recreate those exact conditions in a test but after debugging i could see the aborted event was being fired even though req.res.complete was = true. so i overrode close here to recreate that condition. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. to clarify, the error occurs when req.res.complete = true and res.readable is still = true. that is happening in the wild but don't know how to force it for a test. i imagine it will depend on a specific sequence of TCP session interactions. |
||
_handle._close(); | ||
// set readable to true event though request is complete | ||
if (res.complete) res.readable = true; | ||
callback(); | ||
}; | ||
res.on('end', common.mustCall(() => { | ||
reqCountdown.dec(); | ||
})); | ||
res.on('aborted', () => { | ||
aborted = true; | ||
}); | ||
res.on('error', common.mustNotCall()); | ||
fstream.on('finish', () => { | ||
assert.strictEqual(aborted, abortRequest); | ||
cleanup(fname); | ||
finishCountdown.dec(); | ||
if (finishCountdown.remaining === 0) return; | ||
abortRequest = false; // next one should be a good response | ||
download(); | ||
}); | ||
}); | ||
req.end(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't all of these timers be
setTimeout(..., common.platformTimeout(n))
instead? Otherwise, is there a more reliable way to trigger this issue (perhaps usingnextTick()
and/orsetImmediate()
or something else)?