Phoenix Channel can't maintain connection with frontend once the connection is established (Error during WebSocket handshake)

1k views Asked by At

I have a problem with WebSocket connection.

I use Phoenix as my API and Vue + phoenix-socket on the frontend.

My browser console looks like this:

receive: ok feed:1 phx_reply (1) {response: {…}, status: "ok"}

Joined successfully {feed: Array(3)}

WebSocket connection to 'ws://localhost:4000/socket/websocket?vsn=1.0.0' failed: Error during WebSocket handshake: Unexpected response code: 403

push: phoenix heartbeat (2) {}

receive: ok phoenix phx_reply (2) {response: {…}, status: "ok"}

WebSocket connection to 'ws://localhost:4000/socket/websocket?vsn=1.0.0' failed: Error during WebSocket handshake: Unexpected response code: 403

and so goes on...

As you can see the connection can be established and data goes through but then it sends errors.

So I checked Phoenix:

[info] CONNECTED TO TweeterApiWeb.UserSocket in 0┬Ás
  Transport: :websocket
  Serializer: Phoenix.Socket.V1.JSONSerializer
  Parameters: %{"token" => "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJ0d2VldGVyX2FwaSIsImV4cCI6MTU5NjkxMjAxNiwiaWF0IjoxNTk0NDkyODE2LCJpc3MiOiJ0d2VldGVyX2FwaSIsImp0aSI6IjViYWFlMDRlLTBjMTYtNDEyMi05Y2VlLWZmMzQ2OWM1YWE1YiIsIm5iZiI6MTU5NDQ5MjgxNSwic3ViIjoiMSIsInR5cCI6ImFjY2VzcyJ9.-ZJMyyEBKd0_nHYUBGdaI0qdHn1nuWtpG8sEUHqikBuWTB2sKw9Sk36OsUpXBS5ozRpe2l2VXq8NI58HydIhZA", "vsn" => "1.0.0"}
[debug] QUERY OK source="tweets" db=0.0ms idle=875.0ms
SELECT t0."id", t0."content", t0."comment_count", t0."retweet_count", t0."like_count", t0."profile_id", t0."inserted_at", t0."updated_at" FROM "tweets" AS t0 WHERE (t0."profile_id" = $1) [1]
[info] JOINED feed:1 in 0┬Ás
  Parameters: %{}
[info] REFUSED CONNECTION TO TweeterApiWeb.UserSocket in 0┬Ás
  Transport: :websocket
  Serializer: Phoenix.Socket.V1.JSONSerializer
  Parameters: %{"vsn" => "1.0.0"}

It looks like the connection is refused because there is no token in params but I don't really understand why.

I only check the authentication when the socket connects, so the token should be completely unnecessary once the connection is established.

Here is my user_socket.ex:

defmodule TweeterApiWeb.UserSocket do
  use Phoenix.Socket

  alias TweeterApi.Accounts
  alias TweeterApi.Accounts.Guardian

  ## Channels
  channel "feed:*", TweeterApiWeb.FeedChannel

  @impl true
  def connect(%{"token" => token}, socket, _connect_info) do
    case Guardian.resource_from_token(token) do
      {:ok, user, _claims} ->
        current_profile = Accounts.get_profile_by(user_id: user.id)

        {:ok, assign(socket, :current_profile_id, current_profile.id)}

      {:error, _reason} ->
        :error
    end
  end

  def connect(_params, _socket, _connect_info), do: :error

  @impl true
  def id(socket), do: "users_socket:#{socket.assigns.current_profile_id}"
end

The channel code:

defmodule TweeterApiWeb.FeedChannel do
  use TweeterApiWeb, :channel

  alias TweeterApi.Tweets

  def join("feed:" <> current_profile_id, _params, socket) do
    if String.to_integer(current_profile_id) === socket.assigns.current_profile_id do
      current_profile_tweets = Tweets.list_profile_tweets(current_profile_id)

      response = %{
        feed:
          Phoenix.View.render_many(current_profile_tweets, TweeterApiWeb.TweetView, "tweet.json")
      }

      {:ok, response, socket}
    else
      {:error, %{reson: "Not authorized"}}
    end
  end

  def terminate(_reason, socket) do
    {:ok, socket}
  end
end

and Vue.js code:

<script>
import UserProfileSection from '@/components/sections/UserProfileSection.vue'
import TimelineSection from '@/components/sections/TimelineSection.vue'
import FollowPropositionsSection from '@/components/sections/FollowPropositionsSection.vue'
import NewTweetForm from '@/components/sections/NewTweetForm.vue'
import { mapGetters } from 'vuex'
import { Socket } from 'phoenix-socket'

export default {
    name: 'AppFeed',
    components: {
        UserProfileSection,
        TimelineSection,
        FollowPropositionsSection,
        NewTweetForm,
    },
    data() {
        return {
            tweets: [],
        }
    },
    computed: {
        ...mapGetters('auth', ['currentProfileId', 'token']),
        ...mapGetters('feed', ['tweets'])
    },
    created() {

    },
    mounted() {
        const WEBSOCKET_URL = 'ws://localhost:4000'

        const socket = new Socket(`${WEBSOCKET_URL}/socket`, {
            params: { token: this.token },
            logger: (kind, msg, data) => {
                console.log(`${kind}: ${msg}`, data)
            },
        })

        socket.connect()

        this.channel = socket.channel('feed:' + this.currentProfileId, {})

        this.channel
            .join()
            .receive('ok', (resp) => {
                console.log('Joined successfully', resp)
                console.log(resp)
                this.tweets = resp.feed
            })
            .receive('error', (resp) => {
                console.log('Unable to join', resp)
            })
    }
}
</script>
1

There are 1 answers

0
bwuak On

In a typical phoenix app assign the user's token is assigned to the window in our layout's body.

<script>window.userToken = "<%= assigns[:user_token] %>"</script>

And for the socket creation in assets/js/socket.js

let socket = new Socket("/socket", {
  params: {token: window.userToken},
})

I believe that when you create your socket: { token: this.token }, this.token is undefined, so it is not sent in params.

Edit: If you don't care about the token, don't pattern match it. If there is no token you will jump into your second connect which will pattern match anything and refuse the connection.