Node.js is a LIAR Sometimes!

Eric Lawless
6 min readApr 18, 2016

--

Let’s talk about Node.js. It has certainly been an experience working with Node over the past 4 years. We’ve had some great times, we’ve had some not so great times, and we’ve had whatever you’d classify this as.

So. Here you are, unassuming Node developer. Let’s say you want to open up a TCP server on port 4000. Great! This is exactly the kind of thing Node is good for. Let’s do it!

Look how easy that was. We’ve got our server going, we can start building our app. Or, wait: we don’t really mind if we’re on port 4000. It’s a nice round number, but I’m sure lots of other stuff uses it. We’ll probably run into trouble here if we don’t do something.

Let’s scan to find an available port! Just give it 4000 as a starting point and we’ll pick something nearby. Probably pretty common, we’ll just Google it. We’d pull something off npm but you saw what happened with left-pad. And look, Mikeal Rogers has a neat bit of example code we can use.

Alright, so we’ll just keep trying to open up TCP servers on different ports until we succeed. Maybe we also came across the Node docs for server.listen() and saw you can pass in 0 as the port to get assigned a random one. We’ve come this far, though, we might as well do it ourselves.

There’s some other interesting stuff in those server.listen docs, we should probably take a quick look to understand how this works. Not that we should expect it not to work. But, you know. Node.

Begin accepting connections on the specified port and hostname. If the hostname is omitted, the server will accept connections on any IPv6 address (::) when IPv6 is available, or any IPv4 address (0.0.0.0) otherwise. A port value of zero will assign a random port.

Okay, great! Leaving out the hostname means we’ll automatically listen on all addresses from all network interfaces, because we’re using the IPv4/IPv6 unspecified (wildcard) addresses 0.0.0.0 and :: respectively.

One issue some users run into is getting EADDRINUSE errors. This means that another server is already running on the requested port.

Exactly what we expect to happen. If there’s another server already running on this port, it’ll throw an error, and we’ll know not to use it.

(Note: All sockets in Node.js set SO_REUSEADDR already)

What’s SO_REUSEADDR about? Back to Google! There’s some Linux man pages but it’s not super clear what this option does. I guess we can search Stack Overflow, and we’ll come across this question.

It looks like this option does different things on different operating systems. I bet you have a MacBook, and I bet it has some great stickers on it, so let’s see what BSD based systems like OS X do.

If SO_REUSEADDR is enabled on a socket prior to binding it, the socket can be successfully bound unless there is a conflict with another socket bound to exactly the same combination of source address and port. Now you may wonder how is that any different than before? The keyword is “exactly”. SO_REUSEADDR mainly changes the way how wildcard addresses (“any IP address”) are treated when searching for conflicts.

Whoa, now. We’re using wildcard addresses. Does this mean our getPort method isn’t going to work if somebody else is bound to a specific address like 127.0.0.1? That doesn’t sound great. Let’s test it out.

Well, that prints “Started both servers!” which is exactly what we don’t want. Good thing we read those docs on a whim and didn’t run into this issue in production where it impacted some internal service! That would have been annoying.

Moving on.

There’s always that port: 0 option but I feel like we need to solve this now out of sheer bloody-mindedness. It’s probably worth figuring out why on earth anybody would want it to work this way. That stack overflow answer sheds some light.

When you close a TCP socket, there may still be pending data in the send buffer, which has not been sent yet but your code considers it as sent, since the send() call succeeded.

TCP is said to be a reliable protocol and losing data just like that is not very reliable. That’s why a socket that still has data to send will go into a state called TIME_WAIT when you close it. In that state it will wait until all pending data has been successfully sent or until a timeout is hit, in which case the socket is closed forcefully.

If SO_REUSEADDR is not set, a socket in state TIME_WAIT is considered to still be bound to the source address and port and any attempt to bind a new socket to the same address and port will fail until the socket has really been closed.

That makes more sense. Node sets the option so we don’t have to sit around retrying our connection if there was an old socket that hasn’t finished closing. Seems a bit odd that there isn’t a separate option for that. It’s still not particularly helpful for our use case, either.

I guess we could just try listening on all possible addresses, since SO_REUSEADDR won’t let us bind to a server on the same host:port combination unless it’s in TIME_WAIT state (in which case we’d consider the socket available regardless). Is there any way to get all possible addresses?

Obviously, we’ll just Google it. Yes, apparently. We’ll clean up the code a little bit, add our wildcards in because we want to check for e.g. 0.0.0.0:4000 as well, and we’ll end up with something like this.

So we walk through the network interfaces and grab all of their addresses. Note the bit about link-local IPv6 addresses, that’ll make it so we can spin up a server on those properly. You can read more on Stack Overflow. It’s good to know I guess.

Here’s what that returns on my machine.

Nice, this is a solid starting point. We can get that list of addresses and try to spin up servers on each of them. If any of them fail, we try the next port. We’re going to use Promises because we’re very cool and we don’t care about being able to cancel things.

So, that seems to work. We don’t deal with timeouts well, which is a problem, but I’m glad we figured something out instead of just pulling down an npm package. They do solve this problem, don’t they? Let’s take a look.

It looks like get-port uses port: 0. Based on some cursory research and some more of those secondary sources we like so much, port: 0 doesn’t guarantee the port you get back is not in use. So this doesn’t quite work. What else do we have?

The node-portfinder library takes a similar approach to ours but only checks ::1, 127.0.0.1, 0.0.0.0, and whatever else you pass it, but not :: or some of the other possible addresses. So it’ll work some of the time. Not too bad.

The find-port library only checks the address you pass it, as does find-free-port, portastic, and endpoint-utils. The getport library only checks 0.0.0.0, along with random-port, port-manager, and I’m sure plenty of others in each of these categories.

So, each of the popular libraries we’ve checked has this bug.

We’ve left some loose ends, like what happens on Linux and Windows, but you can probably piece it together from the links and some experimentation. Try spinning up servers on all your local addresses at once and send them all data to see what happens on different platforms.

We still like Node, I think. Things don’t get better unless you talk about stuff that’s wrong. It really was easy to get up and running, and with this much rope we can either hang ourselves or build a tire swing.

--

--