Timeout handle_info function not triggering. I would like to response a custom websocket status code when timed out

Hello, It is my first development. I want to close connection using the 1006 websocket status code with ‘no_ping’ reason when a timeout occurs. Following the documentation I did not get the expected result.

In my websocket.ex, I have a code like that:

defmodule MyApp.Websocket do
  @behaviour Phoenix.Socket.Transport

  def child_spec(_opts) do
    :ignore
  end

  def connect(state) do
    {:ok, state}
  end

  def init(state) do
    {:ok, state}
  end

  def handle_in({input, _opts}, state) do
    {:reply, :ok, {:text, "hi"}, state}
  end

  def handle_info(:timeout, state) do
    IO.puts("timeout")
    {:stop, :normal, {1006, "no_ping"}, state}
  end

  def handle_info({:send, channel, message}, state) do
    {:push, {:text, "hi"}, state}
  end

  def terminate(reason, state) do
    :ok
  end
end

In my endpoint.ex, I have the timeout defined to 10 seconds:

defmodule MyApp.Endpoint do
  use Phoenix.Endpoint, otp_app: :myapp

  @session_options [
    store: :cookie,
    key: "test",
    signing_salt: "test",
    same_site: "Lax"
  ]

  socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]

  socket "/v1", MyApp.Websocket,
    websocket: [timeout: 10_000, path: "/ws", connect_info: [:peer_data, :x_headers]],
    longpoll: false

  plug Plug.Static,
    at: "/",
    from: :myapp,
    gzip: false,
    only: MyApp.static_paths()

  if code_reloading? do
    plug Phoenix.CodeReloader
  end

  plug Phoenix.LiveDashboard.RequestLogger,
    param_key: "request_logger",
    cookie_key: "request_logger"

  plug Plug.RequestId
  plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]

  plug Plug.Parsers,
    parsers: [:urlencoded, :multipart, :json],
    pass: ["*/*"],
    json_decoder: Phoenix.json_library()

  plug Plug.MethodOverride
  plug Plug.Head
  plug Plug.Session, @session_options
  plug MyApp.Router
end

As explained on documentation, I expect that a timeout should go into my handle_info(:timeout, state), prints “timeout” and return a 1006 “no_ping” as disconnection reason, but when I connect using wscat:

wscat -c ws://localhost:4000/v1/ws

and wait 10 seconds, the response is:
Disconnected (code: 1000, reason: “”)

What I am doing wrong? I would like to receive a:
Disconnected (code: 1006, reason: “no_ping”)

Not sure … but I had a quick look at the docs you referenced and noticed you are not returning the correct tuple in your handle_info/2:

The message is a term. It must return one of:

{:ok, state} - continues the socket with no reply
{:push, reply, state} - continues the socket with reply
{:stop, reason, state} - stops the socket

Your code has a four arg tuple:

def handle_info(:timeout, state) do
    IO.puts("timeout")
    {:stop, :normal, {1006, "no_ping"}, state}
 end

I have not tested it or had an experience with this type of test, but I thought I would point it out in case it helped. Good luck

Thank you for your answer, but specifically the ‘four arg tuple’ works, at least from handle_in. It was the only way I found to return the status code and reason. For instance here:

  def validate_and_decode_json(input) do
    case Jason.decode(input) do
      {:ok, json} -> {:ok, json}
      _ -> {:stop, :normal, {1007, "invalid_json"}, :init}
    end
  end

Testing:

 λ wscat -c ws://localhost:4000/v1/ws
Connected (press CTRL+C to quit)
> x
Disconnected (code: 1007, reason: "invalid_json")

I think that the main problem on my code is that the timeout is not entering in handle_info function because the IO.puts("timeout") is not printing on console, so the function output is not a problem (yet).

That’s true for handle_in but not true for handle_info

I am not an expert on websocket communication, but I think you need to be sending your code in response to a keep alive message from the client. If you are simply terminating the connection on your side unilaterally then next time the client tries to ping there is nothing there to reply at all.

The behaviour is: if my client does not pings me (or send other messages) inside the timeout time span, my intention is do a disconnect unilaterally from server. The next time the client tries to send a message, the connection will not be opened.

My intention is send the disconnection reason by “disconnection process”, informing this through websocket status codes, as already worked with my JSON validation exemplified above.

I changed the handle_info to exit with a {:stop, "no_ping", state} to test, but the main point is that when it comes to timeout, I expected this timeout “event” to fall inside the handle_info(:timeout, ... but that’s not happening. Regardless of my function output option, whether I disconnect or send a message, the IO.puts("timeout") line is not displaying anything in the console, that is, the code is not falling into this function.