Handling WebSockets 308 Redirect (using Gun 1.3)

Hi,

I’m trying to use the Gun library to connect to Polygon’s socket server, wss://socket.polygon.io/stocks.

This is the response I expect:

> websocat wss://socket.polygon.io/stocks
[{"ev":"status","status":"connected","message":"Connected Successfully"}]

I’ve written a basic wrapper on the Gun library’s methods, as follows:

  def connect(ws), do: ws_upgrade(ws)

  defp ws_upgrade(%{ path: path, port: port, host: host } = state) do
    {:ok, _} = :application.ensure_all_started(:gun)

    connect_opts = %{
      connect_timeout: :timer.minutes(1),
      retry: 10,
      retry_timeout: 300
    }

    case open_connection(host, port, connect_opts) do
      {:ok, gun_pid} ->
        stream_ref = :gun.ws_upgrade(gun_pid, path)
        %{state | stream_ref: stream_ref, gun_pid: gun_pid}
      {:error, _ } ->
        state
    end
  end

  defp open_connection(host, port, connect_opts) do
    with {:ok, conn_pid} <- :gun.open(host, port, connect_opts),
         {:ok, _protocol} <- :gun.await_up(conn_pid, :timer.minutes(1)) do
      {:ok, conn_pid}
    else
      {:error, reason} ->
        {:error, reason}
    end
  end

I’ve also added Gun response handlers based on this example.

I’ve tested this with basic examples, like 'echo.websockets.org' on port 80, and it works just fine. However, when I try with 'socket.polygon.io' with the path "/stocks", I get a gun_response message with a 308 permanent redirect code. The new location is given by the headers as https://socket.polygon.io/stocks.

This is a bit confusing to me because WebSockets are only available for HTTP/1.1, or at least that’s what the Gun documentation told me, and this seems to want to redirect me to HTTP/2. Has anyone else had to deal with WebSocket redirects like this before? If so, how did you do it? Thanks!

If you make an HTTP request to socket.polygon.io/stocks (port 80) you get a permanent redirect to an HTTPS endpoint, which incidentally seems to be served by a HTTP/2 capable webserver, but HTTP/2 is not playing a role in the redirect.

This host is forcing use of TLS, that’s why you see the redirect. If you connect to websocket over TLS (wss, port 443) it seems to work all fine (trying with the websocat command line client and I can connect to wss://socket.polygon.io/stocks).

2 Likes

Thanks Luca,

Turns out that yes, it was forcing me to use TLS, and Gun’s default behavior when given port 443 is to upgrade to HTTP/2 automatically. Gun’s documentation even mentions that you have to “force HTTP/1.1” in those cases, although the documentation doesn’t make it clear how.

For future readers, here’s what I did to get it working:

  def ws_upgrade(%{ path: path, port: port, host: host } = state) do
    {:ok, _} = :application.ensure_all_started(:gun)

    connect_opts =
      ssl_options(state)
      |> Enum.into(%{
        connect_timeout: :timer.minutes(1),
        retry: 10,
        retry_timeout: 300
      })

    case open_connection(host, port, connect_opts) do
      {:ok, gun_pid} ->
        stream_ref = :gun.ws_upgrade(gun_pid, path)
        %{state | stream_ref: stream_ref, gun_pid: gun_pid}
      {:error, _ } ->
        state
    end
  end

  # When trying to connect to WSS, restrict the protocol to
  # HTTP/1.1, per https://ninenines.eu/docs/en/gun/1.3/guide/websocket/
  defp ssl_options(%{ port: 443 }) do
    %{ protocols: [:http] }
  end
  defp ssl_options(_), do: %{}

# ... other code here
3 Likes