-
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
dgram: improve "address" parameter behavior in Socket.prototype.send #10473
Conversation
I think it would probably be better to just support the 'callback without address' case instead of adding a note/warning in the documentation. |
@mscdex I agree. I don't see any technical reason why we couldn't. I didn't want to be presumptuous. I'll update this PR accordingly. |
@mscdex I've updated this PR as suggested, rebased, etc. My local tests now pass 100%. |
lib/dgram.js
Outdated
if (typeof address === 'function') { | ||
callback = address; | ||
address = undefined; | ||
} else if (typeof address !== 'undefined' && typeof address !== 'string') { |
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 can use address !== undefined
instead of typeof address !== 'undefined'
.
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 it be != undefined
?
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 don't think it matters in this case since it would just fail the other part of the conditional if it was 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.
To be more clear, shouldn't it be if(address != undefined && typeof address != 'string')
--- that is, any of the 3 values undefined
, null
, or String would be acceptable.
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 not sure what you mean by 'acceptable,' but what I was saying is that
address !== undefined && typeof address !== 'string'
and
address != undefined && typeof address !== 'string'
will both evaluate to true
when address === null
. So the change in strictness of the equality of the first comparison does not matter.
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 going to change this to:
} else if (address !== undefined && typeof address !== 'string') {
lib/dgram.js
Outdated
callback = address; | ||
address = undefined; | ||
} else if (typeof address !== 'undefined' && typeof address !== 'string') { | ||
throw new TypeError('"Address" argument must be a string or undefined'); |
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.
Even though it goes against the common error message formatting used in the rest of node core, I think changing this to 'Address must be a string or undefined'
might be better since it is more consistent with the error message format when validating the port
up above.
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'll take a closer look at the wording and punctuation. IMO we should be shooting for consistency with core error messaging; not inconsistent error messaging which may be used elsewhere in this module.
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 don't see test cases for combinations of null, undefined, and empty string being passed as addresses, with and without offset+length, and with and without a callback. This is a lot of combinatorics, btw, I've no strong opinions, but perhaps it may be best to write the test as one test file for dgram.send() arg processing?
lib/dgram.js
Outdated
if (typeof address === 'function') { | ||
callback = address; | ||
address = undefined; | ||
} else if (typeof address !== 'undefined' && typeof address !== 'string') { |
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 it be != undefined
?
doc/api/dgram.md
Outdated
DNS will be used to resolve the address of the host. If the `address` is not | ||
specified or is an empty string, `'127.0.0.1'` or `'::1'` will be used instead. | ||
DNS will be used to resolve the address of the host. Otherwise, if `address` is | ||
not specified or is an empty string, `'127.0.0.1'` (`udp4`) or `'::1'` (`udp6`) |
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 does "not specified" mean? I think in js we "provide" function arguments, not "specify" them. That aside, I don't understand from the text the behaviour of:
send("msg", 7)
// "not specified", I think- `send("msg", 7, ()=>{}) // also not specified, I think
- `send("msg", 7, undefined) // specified as undefined, and handled by code below, I think
- `send("msg", 7, undefined, ()-.{}) // ditto
- `send("msg", 7, "") // same as if address is undefined
send("msg", 7, null) // not same as if address is
undefined`? This is weird and inconsistent, if true
I would state that if address is not provided, or is null, undefined, or an empty string, it will use the protocol specific localhost address - and implement and test this condition if it is not already
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 think @mscdex addressed your comments on line 352. Not sure why I'm seeing it twice 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.
I can change the wording to "provide".
For an address
that is not provided, we defer to the accepted types in dns.lookup()
. Per the code there, address
is allowed to be a falsy value, or a string.
So, I may have been a bit too stringent in my checks. The allowed values of address
are then:
- a nonempty string (which is considered "provided"; the rest are not)
false
undefined
null
0
''
If it's any other truthy value (except Function
; see below), it's invalid, and send()
should throw a TypeError
.
Furthermore, if address
is a Function
, then it's the callback
, and the address
will be reset to a falsy value (I think it was undefined
, but it doesn't really matter).
"Falsy" and "truthy" seem to be unique to JS and there's precedence in the Node.js docs for their usage. Explicitly enumerating all "falsy" values is cumbersome, anyway.
I propose the following changes, then:
- Avoid ambiguity by stating "If
address
is a non-empty string, use it to resolve the host. Ifaddress
is falsy,127.0.0.1
(etc) will be used instead.` - Update my type check(s) to allow all falsy values to pass through to
dns.lookup()
. - Revisit tests to ensure the "falsy" values don't throw exceptions, and truthy non-strings do throw exceptions.
Sound fair?
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.
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.
@boneskull I don't see the usefulness in permitting values like false
and 0
and treating them as undefined
, if that is what you are suggesting?
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 he is saying that is current behaviour, and not doing it would be semver-major. I would say if we are going to change the API, I would expect only allow null
and undefined
to mean "no value". Btw, from above, it looks like 42
would also be considered a host name? I.e., numbers are allowed? Do they get converted to strings, like '42'
and passed to the resolver?
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.
@boneskull I don't see the usefulness in permitting values like false and 0 and treating them as undefined, if that is what you are suggesting?
This is the current behavior. Values are passed through to dns.lookup()
, which allows such things. The documentation for dgram
is therefore inaccurate. And ambiguous, which prompted the change.
I think he is saying that is current behaviour, and not doing it would be semver-major. I would say if we are going to change the API, I would expect only allow null and undefined to mean "no value"
This is correct. I'm not proposing making a semver-major change here. Whether or not allowing false
is "useful", I don't know, but I'd argue that's not a big enough deal to warrant a semver-major change.
Btw, from above, it looks like 42 would also be considered a host name? I.e., numbers are allowed? Do they get converted to strings, like '42' and passed to the resolver?
No. The only valid number
is 0
, which is falsy. Then:
If it's any other truthy value (except Function; see below), it's invalid, and send() should throw a TypeError.
Btw, labelled as minor because of
but perhaps it should be major because of
And of course if it starts rejecting previously valid input, it must be major, but that's still under discussion, I think. |
It doesn't. Either would throw an error; the errors thrown in this PR are more helpful/obvious. UPDATE By "fall through" I mean the |
@boneskull Changes in error message text are considered semver-major, because they can break apps that match against |
@boneskull FYI, I would have done this as one commit introducing tests, because the test coverage wasn't there over the invalid addresses, asserting the current behaviour, then a follow-on commit that improves the error messages. That would as a side-effect also make whether it is an API change or not crystal clear - because changes to test behaviour are API changes. Its probably too much work to rebase it like that, but a 2-phase approach for code that has no test coverage ATM is easier to review. |
That's an unfortunate--but likely necessary--policy. I want to make it clear that I'm not interested in expanding this PR to further restrict allowed types or values of the |
This is actually pretty straightforward to do. |
I understand. This PR doesn't change the behaviour, other than making the error messages much more helpful. The tests and docs do reveal that the current behaviour is not ideal, and now that you have done this work, its easier to discuss how we wish the API would work, and change it in the future. |
That's not entirely true either. This PR also allows // previous to this PR, the following would throw
socket.send(buf, port, () => {
console.log('done');
}); |
Yes. This is silly, but would work: socket.send(buf, port, '', () => {
console.log('address was 127.0.0.1');
}); What's sillier is that currently you'd have to write something like the above code if you wanted a callback and didn't care to specify the address. 😝 |
To recap, my plan is:
|
@mscdex @sam-github Please take another gander. |
I also realized this squashes a particularly gross bug--if You can see this problem illustrated in the tests of my first commit. I was not able to assert the exceptions were thrown unless I did this (or caught them at the |
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.
Two small nits, but otherwise LGTM
doc/api/dgram.md
Outdated
specified or is an empty string, `'127.0.0.1'` or `'::1'` will be used instead. | ||
DNS will be used to resolve the address of the host. If `address` is not | ||
provided or otherwise falsy, `'127.0.0.1'` (`udp4`) or `'::1'` (`udp6`) will | ||
be used by default. |
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.
.... depending on whether socket is
'udp4'
or'udp6'
.
^--- I think its worth stating explicitly how it the or
is determined.
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 this is done
const dgram = require('dgram'); | ||
const client = dgram.createSocket('udp4'); | ||
|
||
const buf = Buffer.allocUnsafe(256); |
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.
The test doesn't rely on sending unspecified data, does it? Can you just use const buf = Buffer.alloc(256, 'x');
as in the other test?
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.
Sure; was just copypasta from yet another test...
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 this is done
conflict resolved |
I'm trying to run CI against this PR and it is not going well. I'm pretty sure the problem is with CI and not this PR, though. Maybe someone else seeing this in another hour or two can try again? |
@Trott Anything I can do to help? |
@boneskull Yes! Can you remove the merge commit? Rumor has it that is the source of the issue. |
@Trott Hm, that's what I get for using GitHub's new conflict resolver tool, I guess. 😄 |
- Add coverage around valid, undocumented types for `address` parameter. - Add coverage around known invalid, but uncovered, types for `address` parameter.
@Trott rebased |
@boneskull This is fair, but not being strict about things like #10473 (comment) have caused breakage in the past. I think you're right though, what we need is to have all this stuff covered by linter rules, so that everything can all be fixed at once. Speaking of which, regarding this (by @cjihrig):
This seems like something we should lint for in |
We should definitely allow validation functions as an alternative too. It's just constructors that can be problematic. There's more to talk about regarding this (specifically if we might be moving towards a situation where error message changes are not breaking changes anymore thanks to the error identifiers that @jasnell has introduced, and if so then whether that means fully-matching regexps will become anti-patterns as the second argument). But this is not the place for that discussion. (Personally, I think we're probably a long way off from being able to treat message changes as non-breaking changes, but I'm often wrong.) |
One potential concern about linting for error constructors is that the rule might report an error for this code: var ERROR_PATTERN = /^Error: Something bad happened$/;
// later...
// as far as the rule is concerned, ERROR_PATTERN could be a constructor
assert.throws(foo, ERROR_PATTERN);
assert.throws(bar, ERROR_PATTERN); It would be relatively simple to check for constructors that are known beforehand, e.g. |
/cc @nodejs/ctc This semver-major has sufficient approvals to land, but it's been a sufficiently long time coming that it might be off the radar for some folks. PTAL if you get a chance. |
|
||
const client = dgram.createSocket('udp4'); | ||
|
||
const messageSent = common.mustCall(function messageSent(err, bytes) { |
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.
assert.ifError(err)
client.send([buf1, buf2], port, messageSent); | ||
}); | ||
|
||
client.on('message', common.mustCall(function onMessage(buf, info) { |
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.
Unused variable info
.
const offset = 20; | ||
const len = buf.length - offset; | ||
|
||
const onMessage = common.mustCall(function messageSent(err, bytes) { |
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.
err
has to be asserted first.
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.
FWIW, these two files are copypasta from test-dgram-send-callback-buffer-length
(etc) (some of) which have the same issues.
OMG the |
- Do not require presence of `address` parameter to use `callback` parameter; `address` is *always* optional - Improve exception messaging if `address` is invalid type - If `address` is an invalid type, guarantee a synchronously thrown exception - Update documentation to reflect signature changes
@thefourtheye Changes applied |
ping anyone |
client.send(buf, port, true); | ||
}, expectedError); | ||
|
||
client.unref(); |
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 just noticed this. Is there any chance that the socket would be unref'ed and the process exit before all six messages are received. I am able to make the test fail artificially by adding a short timeout around the last successful send. It doesn't seem to be a problem on the CI, but wouldn't want it to be a source of future flakiness either.
- Do not require presence of `address` parameter to use `callback` parameter; `address` is *always* optional - Improve exception messaging if `address` is invalid type - If `address` is an invalid type, guarantee a synchronously thrown exception - Update documentation to reflect signature changes - Add coverage around valid, undocumented types for `address` parameter. - Add coverage around known invalid, but uncovered, types for `address` parameter. PR-URL: nodejs#10473 Reviewed-By: Sam Roberts <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Colin Ihrig <[email protected]>
Landed in 32679c7. |
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passesAffected core subsystem(s)
dgram
Description of change
When using
Socket.prototype.send
, I was confused about whenaddress
was required, since the docs are ambiguous at best, and conflicting at worst: they claimaddress
will default to127.0.0.1
, but this is only in the case wherecallback
is not supplied.My confusion manifest in an exception from the
dns
module, which was unhelpful:dns.lookup
expects a validhostname
argument, yet this parameter name ("hostname") is not used inSocket.prototype.send
. I thought it better to fail with helpful error messaging before getting as far asdns.lookup
, which prompted this PR.The signature ofSocket.prototype.send
is a bit awkward, and perhaps the best fix here is to change it. I see this PR as being an improvement nonetheless. Description of changes follow.UPDATE Jan 8 17
Upon suggestion by @mscdex, I've modified the changes to make
address
optional in all cases.Summary of changes:
Socket.prototype.send
,address
is now always optional; it is no longer required to usecallback
. The signature has changed from this:address
argument is present but invalid; exceptions no longer fall through todns
module.