Gen_tcp accept socket across processes gives `:einval`

Hi everyone,

I’m implementing something that needs to read a TCP stream for the first time and trying to get my head around gen_tcp. I start a GenServer to handle the connection and want to be able to make other calls to it so I thought spawning the blocking :gen_tcp.accept call would be efficient but it seems the socket doesn’t survive being transferred between processes. Does anyone know why? Or if I’m making an elementary mistake here?

Here’s a minimal script that demonstrates the problem:

# testtcp.exs
{:ok, socket} = :gen_tcp.listen(0, [])
{:ok, port} = :inet.port(socket) |> dbg()
current = self()
# Spawn a process just for accepting connections and send the new connection back to use when it
# happens.
spawn(fn ->
  case :gen_tcp.accept(socket) do
    {:ok, conn} ->
      dbg(:inet.sockname(conn))
      send(current, {:new_conn, conn} |> dbg())
  end
end)

# Spawn to connect to the socket.
spawn(fn ->
  dbg(:gen_tcp.connect('localhost', port, [], :infinity))
end)

receive do
  {:new_conn, conn} ->
    dbg({:received_conn, conn})
    dbg(:inet.sockname(conn))
end

Which outputs:

[tcptest.exs:3: (file)]
:inet.port(socket) #=> {:ok, 40015}

[tcptest.exs:8: (file)]
:inet.sockname(conn) #=> {:ok, {{127, 0, 0, 1}, 40015}}

[tcptest.exs:14: (file)]
:gen_tcp.connect('localhost', port, [], :infinity) #=> {:ok, #Port<0.7>}

[tcptest.exs:9: (file)]
{:new_conn, conn} #=> {:new_conn, #Port<0.8>}

[tcptest.exs:19: (file)]
{:received_conn, conn} #=> {:received_conn, #Port<0.8>}

[tcptest.exs:20: (file)]
:inet.sockname(conn) #=> {:error, :einval}

(there’s no way to add line numbers to code blocks here right?)

You’ll notice the dbg output is slightly out of order, but it shows the change between calling :inet.sockname in the same process that accepted the socket (valid) and in the receive loop ({:error, :einval}).

Thanks for any help!
Andrew