-
Notifications
You must be signed in to change notification settings - Fork 29.7k
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: throw an error for unexpected agent values #10053
Conversation
@@ -35,6 +34,13 @@ function ClientRequest(options, cb) { | |||
} else if ((agent === null || agent === undefined) && | |||
typeof options.createConnection !== 'function') { | |||
agent = defaultAgent; | |||
} else if (agent !== null && agent !== undefined && | |||
!(agent instanceof Agent.Agent)) { | |||
console.log(agent, typeof agent); |
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.
Please remove the console.log.
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.
doh, thats embarassing. Will remove.
@@ -35,6 +34,13 @@ function ClientRequest(options, cb) { | |||
} else if ((agent === null || agent === undefined) && | |||
typeof options.createConnection !== 'function') { | |||
agent = defaultAgent; | |||
} else if (agent !== null && agent !== undefined && | |||
!(agent instanceof Agent.Agent)) { |
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.
What is the reason for these specific checks vs. a simple } else {
?
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.
We should accept an instance of Agent.Agent. Had to check for the null value as well because the previous if statement also requires options.createConnection to not be a function.
This if block could probably be rewritten slightly to account for all acceptable values and then throw the error in a simple else clause.
console.log(agent, typeof agent); | ||
throw new Error( | ||
'Agent option must be an instance of http.Agent, undefined or false. ' | ||
+ 'Received type: ' + typeof agent + ' instead' |
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.
Tiny nit: can you indent by four spaces here? It's allowed to use string templates (backticks) if you think that's easier to read.
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.
cool.
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.
@cjihrig requested moving the + to the previous line, once doing that I got confused by the request for 4 spaces. Should the second line of text be indented another level?
method: 'GET', | ||
port: undefined, | ||
host: '127.0.0.1', | ||
agent: true, |
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.
Can you test some other values as well?
|
||
try { | ||
http.request(options); | ||
} catch (err) { |
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.
Please use assert.throws() instead of try/catch. This should fail but won't when http.request doesn't throw an exception.
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.
👍 definitely
@@ -0,0 +1,21 @@ | |||
'use strict'; | |||
require('../common'); | |||
var assert = require('assert'); |
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.
Can you use const
instead of var
in this file.
assert.strictEqual( | ||
err.message, | ||
'Agent option must be an instance of http.Agent, undefined or false. ' | ||
+ 'Received type: boolean instead' |
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.
Can you move the +
to the previous line.
@cjihrig @bnoordhuis - Thanks for your feedback. I've addressed most of your remarks. I made some further changes to the logic inside of request. |
Will squash the commits down prior to merge once approved and passing CI. |
res.end(); | ||
}); | ||
|
||
server.listen(0, baseOptions.host, function() { |
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.
Can you wrap this callback in common.mustCall()
.
() => http.request(Object.assign(baseOptions, {agent})), | ||
(err) => { | ||
return err.message === 'Agent option must be an instance of ' + | ||
'http.Agent, undefined or false. Received type: ' + |
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.
This line, and the following line, should be indented more.
); | ||
}); | ||
|
||
const intervalId = setInterval(() => { |
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.
Instead of using a timer (which tends to be flaky), and counting requests received, can you use common.mustCall()
with it's second option to define how many times the server request handler is run.
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.
That would make the process.on('exit', ...)
handler unnecessary as well.
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.
@cjihrig - what is the correct way to .close() the server with this kind of test. Ideally we wouldn't close it just after request x in case for some awful reason x + 1 request would have erroneously occurred which would need to be caught?
I've been looking in some of the other http type tests but all seem to implement something different.
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.
Also 👍 on using the .mustCall function. I'll read through common.js in the future for test helpers.
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.
I guess you might still need the counter to close the server. You might be able to get away with just closing the server after verifying that no exception was thrown, but that probably isn't a good idea. You could also use something like Promise.all()
I suppose.
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.
Got a solution up that i think will work, if you could re-review i would appreciate it. Thanks!
@@ -14,7 +14,6 @@ const OutgoingMessage = require('_http_outgoing').OutgoingMessage; | |||
const Agent = require('_http_agent'); | |||
const Buffer = require('buffer').Buffer; | |||
|
|||
|
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.
Unnecessary/unrelated change.
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.
Addressed in a new commit and squashed
agent = defaultAgent; | ||
} | ||
|
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.
Unnecessary/unrelated change.
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.
Addressed in a new commit and squashed
40b750e
to
1bbfa4c
Compare
if (agent === false) { | ||
agent = new defaultAgent.constructor(); | ||
} else if ((agent === null || agent === undefined) && | ||
typeof options.createConnection !== 'function') { | ||
} else if (agent == null && typeof options.createConnection !== 'function') { | ||
agent = defaultAgent; | ||
} |
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.
Instead of duplicating all the checks on line 34, couldn't you just add:
} else if (!(agent instanceof Agent.Agent)) {
throw new TypeError(...);
}
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.
It's possible for agent to be null at this point, so I could do an else if here and check that it's not null and that it's not an instance of Agent.Agent.
I went this route as it verifies that we are dealing with appropriate types before this if statement and because the else if logic seemed slightly verbose.
I am okay with putting the extra checks on the else if statement though if it's preferred.
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.
I think it's worth mixing into this logic in order to avoid duplicating all the checks.
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.
@cjihrig how about this logic structure:
if (agent === false) {
// noop
} else if (agent == null) {
if (typeof options.createConnection !== 'function') {
// noop
}
} else if (!(agent instanceof Agent.Agent)) {
// noop throw error.
}
if agent is undefined or null it'll be cause by the second logic gate, and if createConnection is a function it will not do anything, and will skip the final check. The last else if is exactly as you defined earlier. Obviously the code snippet has implementation removed just for brevity.
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.
Seems like that would hit all of the cases.
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.
Just pushed that change, all tests passing locally 👍 thanks for the help @cjihrig
closeServer = true; | ||
} | ||
request.on('response', common.mustCall(() => { | ||
if (closeServer) server.close(); |
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.
Couldn't you just increment numberOfRequests
here, and remove the closeServer
variable. Also, can you move server.close()
to a new line.
EDIT: You shouldn't really need to track the number of failures, since they should be captured by assert.throws()
.
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.
numberOfRequests is counting the number of requests being initiated. Some of them are actually not actually hitting the server as they fail.
Definitely can move the server.close to new line
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.
@cjihrig addressed this in a new commit. Tracking now just the number of responses to close the server after receiving the expected number. Thanks
1bbfa4c
to
bace99e
Compare
Marking semver-major because of the added throw |
bace99e
to
c0f4d20
Compare
@bnoordhuis at your convenience would you mind taking a gander at this again? |
@@ -30,11 +30,18 @@ function ClientRequest(options, cb) { | |||
|
|||
var agent = options.agent; | |||
var defaultAgent = options._defaultAgent || Agent.globalAgent; | |||
|
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.
unrelated whitespace change
} else if ((agent === null || agent === undefined) && | ||
typeof options.createConnection !== 'function') { | ||
agent = defaultAgent; | ||
} else if (agent == null) { |
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.
unrelated refactor. including purely stylistic changes in the same commit as a functional change makes PRs harder to review, and your commit message doesn't mention or justify these changes. These should be two commits, the addition of the else if (!(agent instanceof Agent.Agent)) {
is the functional change, the other commit would be the style changes (though I would drop the addition of the random whitespace line, that's just code churn).
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.
@sam-github i can actually not change this else if statement/block at all if i also check that agent is != null on my new else if (the functional change you mentioned). Other reviewers said that we should avoid duplicating the checks. What would be best in this case? I can split it into two commits as well just want to make sure i'm following best practices.
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.
I misread the code, I see now, you pulled the check on createConnection out of the conditional, so that if it is a function, no check is made on the type of agent. Code like this is a bit hard to read, I suggest just adding a comment between line 39 and 40 below, making it clear that this case is explicitly being dropped through - that agent can be == null
if there is a createConnection function, that would have caused me to not misread.
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.
@sam-github at your convenience could you check the comment. Wasn't too sure on the wording so it seems a little verbose to me given i was struggling on explaining the case. I'm open to rewrite. Thanks again for your feedback.
const failingAgentOptions = [ | ||
true, | ||
'agent', | ||
{}, |
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.
would be good to inject a couple other js types here: Function, Number, Symbol
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.
added those types
new http.Agent(), | ||
]; | ||
|
||
|
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.
random whitespace - you have single line between global scope vars everywhere but here
c0f4d20
to
81d4f43
Compare
} else if ((agent === null || agent === undefined) && | ||
typeof options.createConnection !== 'function') { | ||
agent = defaultAgent; | ||
} else if (agent == null) { |
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.
Please use (agent === null)
(strict equals)
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.
@jasnell it looks to me that it is required to be non-strict to match undefined
and null
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.
@sam-github @jasnell its non strict to catch both types but i did take the liberty of making that change whereas original version strictly checked for both null and undefined. So i'm flexible on implementation here.
// explicitly pass through this statement as agent will not be used | ||
// when createConnection is provided. | ||
} else if (!(agent instanceof Agent.Agent)) { | ||
throw new Error( |
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.
Please use TypeError
here
failingAgentOptions.forEach((agent) => { | ||
assert.throws( | ||
() => createRequest(agent), | ||
(err) => { |
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.
instead of a function here, just use a regexp... e.g.
assert.throws(() => createRequest(agent),
/^TypeError: Agent option must be an instance of http.Agent/);
acceptableAgentOptions.forEach((agent) => { | ||
assert.doesNotThrow( | ||
() => createRequest(agent), | ||
Error, |
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.
TypeError
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.
@brad-decker What I mean is that you can simply test assert.doesNotThrow(() => createRequest(agent))
, there is no need to check for error types.
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.
Almost there... just a few bits remaining
if (typeof options.createConnection !== 'function') { | ||
agent = defaultAgent; | ||
} | ||
// Case when agent == null && typeof createConnection === 'function' |
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.
This line just restates the code, I would drop it, and capitalize the "Explicitly" on the next sentence, so its clear that not handling this case is intentional.
Other than the one line of the comment that I would drop, LGTM |
81d4f43
to
0229c2d
Compare
@bnoordhuis fixed the last bit of feedback you had, thanks again for reviewing and my apologies for the delay! |
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.
LGTM, thanks. CI: https://ci.nodejs.org/job/node-test-pull-request/5473/
@bnoordhuis - Can you point me in the direction of any resources that might help me figure out the CI testing process so i can address issues with the /arm and /linux tests? Would love to become more familiar with this process so i can be self sufficient here. As it stands i'm not quite sure what has failed. |
This is the only failure in the CI run - https://ci.nodejs.org/job/node-test-commit-linux/6776/nodes=ubuntu1610-x64/console (search for "not ok"). It doesn't appear to be related to the changes in this PR. |
Cool. Thanks @cjihrig - ill wait for feedback that specifies otherwise prior to delving into this. |
} else if (!(agent instanceof Agent.Agent)) { | ||
throw new TypeError( | ||
'Agent option must be an instance of http.Agent, undefined or false. ' + | ||
`Received type: ${typeof agent} instead` |
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.
I'm really not that fond of the Received type: ...
additional bits here.
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.
@jasnell - sorry for the delay. Addressed your feedback and removed the extra bits.
67b9eda
to
86f373e
Compare
@brad-decker can you run |
@italoacasas i don't get any errors when running make lint. I also examined the output of the linter failure in CI and it seems to be a build error rather than a true lint error? I may be wrong i'm pretty new to this whole contributing to node thing. |
@brad-decker I'm getting an error when I patch your PR onto master, could you rebase and see if you get an error? ➜ curl -L https://github.com/nodejs/node/pull/10053.patch | git am --whitespace=fix"
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 139 0 139 0 0 341 0 --:--:-- --:--:-- --:--:-- 342
100 3369 0 3369 0 0 3770 0 --:--:-- --:--:-- --:--:-- 24237
Applying: http: throw an error for unexpected agent values
➜ node git:(master) make lint
./node tools/eslint/bin/eslint.js --cache --rulesdir=tools/eslint-rules \
benchmark lib test tools
/Users/gib/wrk/com/node/test/parallel/test-http-client-reject-unexpected-agent.js
52:8 error Expected indentation of 6 spaces but found 7 indent
✖ 1 problem (1 error, 0 warnings)
make: *** [jslint] Error 1 |
failingAgentOptions.forEach((agent) => { | ||
assert.throws( | ||
() => createRequest(agent), | ||
/^TypeError: Agent option must be an instance of http.Agent/, |
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.
Looks like one too many spaces on this line
As per nodejs#9069 unexpected things can happen when supplying an unexpected value to agent. Beings as the docs clearly state the expected values, this throws an error on an unexpected value. Signed-off-by: brad-decker <bhdecker84@gmail.com>
@italoacasas @gibfahn I see what i did. i rebased on origin and not upstream :P got the lint error after rebasing. Sorry about that. fixed now |
86f373e
to
25cd753
Compare
CI is green (ignore the test/arm reported failure) |
Landed fc7025c |
As per #9069 unexpected things can happen when supplying an unexpected value to agent. Beings as the docs clearly state the expected values, this throws an error on an unexpected value. PR-URL: #10053 Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Sam Roberts <vieuxtech@gmail.com>
Thanks everyone! |
As per nodejs#9069 unexpected things can happen when supplying an unexpected value to agent. Beings as the docs clearly state the expected values, this throws an error on an unexpected value. PR-URL: nodejs#10053 Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Sam Roberts <vieuxtech@gmail.com>
} | ||
// Explicitly pass through this statement as agent will not be used | ||
// when createConnection is provided. | ||
} else if (!(agent instanceof Agent.Agent)) { |
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.
How about userland Agent
? Those Agent Class maybe not inherits from Agent.Agent
and now will be all fails to use them. e.g.: TunnelingAgent https://github.com/request/tunnel-agent/blob/master/index.js#L47
Checklist
make -j8 test
(UNIX), orvcbuild test nosign
(Windows) passesAffected core subsystem(s)
http, test
Description of change
As per #9069 unexpected things can happen when supplying
an unexpected value to agent. Beings as the docs clearly
state the expected values, this throws an error on an
unexpected value.