Phoenix server on EC2 can receive GET requests but not a WebSocket connection.

Problem Description

I wrote a simple game server in Phoenix that uses Channels to network positions of the players. The client is a JavaScript app—written in Node and bundled with WebPack—and it uses Phoenix.js to connect to the server.

Using Docker, the server was deployed to an EC2 instance. I made sure to use 0.0.0.0 as the IP, and in Docker, published the port 4000:

docker run -dp 4000:4000 --env-file .env phoenix-backend

When I make a GET request to the server, I get a 404 response—which makes sense, because I’ve removed all but Channels from the app. If I view the Docker logs, I can see it received the request and responded with a 404.

However, I cannot connect over WebSocket. The logs don’t even register that an attempt was made.

check_origin is disabled and the EC2 instance is configured to receive all traffic from everywhere.

What Has Been Tried

I wanted to test connecting locally, but I wasn’t able to figure out how to run Phoenix.js on the EC2 instance. It doesn’t have a browser, and when I tried in Node, I got the following error:

/home/ec2-user/phoenix-client-test/node_modules/phoenix/priv/static/phoenix.cjs.js:764
    this.transport = opts.transport || global.WebSocket || LongPoll;
                                              ^

TypeError: Cannot read properties of undefined (reading 'WebSocket')
    at new Socket (/home/ec2-user/phoenix-client-test/node_modules/phoenix/priv/static/phoenix.cjs.js:764:47)
    at Object.<anonymous> (/home/ec2-user/phoenix-client-test/index.js:3:16)
    at Module._compile (node:internal/modules/cjs/loader:1099:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
    at Module.load (node:internal/modules/cjs/loader:975:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12)
    at node:internal/main/run_main_module:17:47

The code itself:

const phoenix = require("phoenix");
const socket = new phoenix.Socket("ws://localhost:4000/socket", {});
socket.connect();

This leads me to believe Phoenix.js cannot be run from a server. If so, I’m all out of ideas. I don’t even know what to test anymore.

The config/ code

prod.exs

import Config
config :al_jeem_backend, AlJeemBackendWeb.Endpoint,
  http: [ip: {0, 0, 0, 0}, port: 4000],
  url: [host: "localhost", port: 4000],
  check_origin: false

config :logger, level: :info

I have tried, here and everywhere else, to change host from localhost to the instance’s public IPv4 DNS. The results stayed exactly the same.

config.exs

import Config
config :al_jeem_backend, AlJeemBackendWeb.Endpoint,
  url: [host: "localhost"],
  render_errors: [view: AlJeemBackendWeb.ErrorView, accepts: ~w(json), layout: false],
  pubsub_server: AlJeemBackend.PubSub,
  live_view: [signing_salt: "dunno if this is sensitive info, so I removed it"]

# Configures Elixir's Logger
config :logger, :console,
  format: "$time $metadata[$level] $message\n",
  metadata: [:request_id]

# Use Jason for JSON parsing in Phoenix
config :phoenix, :json_library, Jason

# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{config_env()}.exs"

runtime.exs

import Config

# Start the phoenix server if environment is set and running in a release
if System.get_env("PHX_SERVER") && System.get_env("RELEASE_NAME") do
  config :al_jeem_backend, AlJeemBackendWeb.Endpoint, server: true
end

if config_env() == :prod do
  secret_key_base =
    System.get_env("SECRET_KEY_BASE") ||
      raise """
      environment variable SECRET_KEY_BASE is missing.
      You can generate one by calling: mix phx.gen.secret
      """

  host = System.get_env("PHX_HOST") || "localhost"
  port = String.to_integer(System.get_env("PORT") || "4000")

  config :al_jeem_backend, AlJeemBackendWeb.Endpoint,
    url: [host: host, port: port],
    http: [
      ip: {0, 0, 0, 0, 0, 0, 0, 0},
      port: port
    ],
    secret_key_base: secret_key_base
end

really? because this very much looks like misconfiguration of the EC2’s security group.

I had hoped so, too, but I have double- and triple-checked it. See the screenshots here.

Do you think I need to add anything else there?

Actually, I tried manually initiating a WebSocket connection with curl, and that seems to do something.

This command…

curl
  --include
  --no-buffer
  --header "Connection: Upgrade"
  --header "Upgrade: websocket"
  --header "Host: example.com:80"
  --header "Origin: http://example.com:80"
  --header "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ=="
  --header "Sec-WebSocket-Version: 13"
  http://my-ec2-id.eu-west-1.compute.amazonaws.com:4000/socket/websocket?vsn=2.0.0

…returns this response:

HTTP/1.1 101 Switching Protocols
cache-control: max-age=0, private, must-revalidate
connection: Upgrade
date: Tue, 22 Mar 2022 14:58:00 GMT
sec-websocket-accept: qGEgH3En71di5rrssAZTmtRTyFk=
server: Cowboy
upgrade: websocket

I don’t know if that’s a success, but at least the attempt is registered in the Docker logs:

14:58:01.541 [info] CONNECTED TO AlJeemBackendWeb.UserSocket in 89µs
  Transport: :websocket
  Serializer: Phoenix.Socket.V2.JSONSerializer
  Parameters: %{"vsn" => "2.0.0"}

Unfortunately, the Phoenix client fails to replicate the result.

Similarly, using Hoppscotch, my API testing tool (like Postman), I can get some sort of response by sending a GET request asking for an upgrade to WebSocket (same arguments as the curl command), but if I actually try the WebSocket testing tool and connect over the wss:// protocol, it fails pretty much instantly as well.

Any idea what the problem might be? I’m not using ELB or anything fancy; just a plain old EC2 instance. I have tried changing the IP Phoenix is listening to from 0.0.0.0 to the instance’s public IP and its private IP, but that doesn’t seem to do a thing.

That is because node does not have the global.webSocket that is for the browser.
You can use the node version phoenix-channels npm. The package is very old and it does not work correctly when used in a req.on(‘data’, ()=>cb) which is what I was trying to use it for. What happens it buffers the push events and runs them at the end for some reason.

What I ended up doing was using a post request with axios to phoenix and then an Endpoint.broadcast. Obviously then if phoenix needed to talk back to the node app it would be though another post request.

In this way the upload progress which is what I was using it for was running smoothly like socketIO. The only issue is that you cannot have a ping pong kind of heart beat using rest.

You are better off trying to use node webSocket library and writing your own interface to phoenix and seeing how that works. I may do this as a todo.

Keep me updated on your progress.