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>
In a typical phoenix app assign the user's token is assigned to the window in our layout's body.
And for the socket creation in assets/js/socket.js
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.