Search code examples
reactjsnode.jsdockerdocker-composewebsocket

WebSocket Connection Fails in docker client but not in local / container


Context

I'm working on a React client app for a chat app using rabbitmq, ws, docker, docker compose, react-use-websocket.

repository here

Problem

The Websocket connection fails from the browser of the react app, but works perfectly fine inside the container when using wscat and also when running the react app locally (not using docker)

Demo

what works

The WebSocket connection from the client to the server works perfectly when I try it from within the client docker container using :

> make up -d
> docker compose exec m1pex-client sh
/app # npm install -g wscat

added 9 packages in 3s
/app # wscat -c ws://my-server:10101
Connected (press CTRL+C to quit)
> exit
Disconnected (code: 1006, reason: "")
/app #

and also we can see the server is listening on 10101:

docker-compose exec m1pex-server sh

/app # netstat -tuln
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 127.0.0.11:38569        0.0.0.0:*               LISTEN
tcp        0      0 :::10101                :::*                    LISTEN
udp        0      0 127.0.0.11:49401        0.0.0.0:*
/app #

what fails

But it fails when the React app within the browser tries to connect to it : (I've console.log many times my .env REACT_APP_WS_URL and it is right)

enter image description here

The full error :

localhost/:1 Error while trying to use the following icon from the Manifest: http://localhost:3050/logo192.png (Download error or resource isn't a valid image)
create-or-join.ts:97 WebSocket connection to 'ws://m1pex-server:10101/' failed: 
exports.createOrJoinSocket @ create-or-join.ts:97
(anonymous) @ use-websocket.ts:106
step @ use-socket-io.ts:73
(anonymous) @ use-socket-io.ts:73
fulfilled @ use-socket-io.ts:73
Show 5 more frames
Show less
App.js:75 WebSocket error: Event {isTrusted: true, type: 'error', target: WebSocket, currentTarget: WebSocket, eventPhase: 2, …}isTrusted: truebubbles: falsecancelBubble: falsecancelable: falsecomposed: falsecurrentTarget: WebSocket {url: 'ws://m1pex-server:10101/', readyState: 3, bufferedAmount: 0, onopen: ƒ, onerror: ƒ, …}defaultPrevented: falseeventPhase: 0returnValue: truesrcElement: WebSocket {url: 'ws://m1pex-server:10101/', readyState: 3, bufferedAmount: 0, onopen: ƒ, onerror: ƒ, …}target: WebSocket {url: 'ws://m1pex-server:10101/', readyState: 3, bufferedAmount: 0, onopen: ƒ, onerror: ƒ, …}timeStamp: 542.3999999761581type: "error"[[Prototype]]: Event
onError @ App.js:75
webSocketInstance.onerror @ attach-listener.ts:87
error (async)
bindErrorHandler @ attach-listener.ts:86
__webpack_modules__../node_modules/react-use-websocket/dist/lib/attach-listener.js.exports.attachListeners @ attach-listener.ts:155
exports.createOrJoinSocket @ create-or-join.ts:105
(anonymous) @ use-websocket.ts:106
step @ use-socket-io.ts:73
(anonymous) @ use-socket-io.ts:73
fulfilled @ use-socket-io.ts:73
Promise.then (async)
step @ use-socket-io.ts:73
(anonymous) @ use-socket-io.ts:73
__webpack_modules__../node_modules/react-use-websocket/dist/lib/use-websocket.js.__awaiter @ use-socket-io.ts:73
start_1 @ use-websocket.ts:88
(anonymous) @ use-websocket.ts:126
invokePassiveEffectCreate @ react-dom.development.js:23487
callCallback @ react-dom.development.js:3945
invokeGuardedCallbackDev @ react-dom.development.js:3994
invokeGuardedCallback @ react-dom.development.js:4056
flushPassiveEffectsImpl @ react-dom.development.js:23574
unstable_runWithPriority @ scheduler.development.js:468
runWithPriority$1 @ react-dom.development.js:11276
flushPassiveEffects @ react-dom.development.js:23447
performSyncWorkOnRoot @ react-dom.development.js:22269
(anonymous) @ react-dom.development.js:11327
unstable_runWithPriority @ scheduler.development.js:468
runWithPriority$1 @ react-dom.development.js:11276
flushSyncCallbackQueueImpl @ react-dom.development.js:11322
flushSyncCallbackQueue @ react-dom.development.js:11309
unbatchedUpdates @ react-dom.development.js:22438
legacyRenderSubtreeIntoContainer @ react-dom.development.js:26020
render @ react-dom.development.js:26103
./src/index.js @ index.js:6
options.factory @ react refresh:6
__webpack_require__ @ bootstrap:24
(anonymous) @ startup:7
(anonymous) @ startup:7
Show 30 more frames
Show less
App.js:72 WebSocket connection closed

Solution (?)

I suspect it might be a CORS issue, but I can't pinpoint the exact problem. Here are more details...

Details

docker-compose.yml:

networks:
  m1pex-network:
    driver: bridge

services:
    m1pex-server:
    build:
      context: ./server
    working_dir: /app
    environment:
      AMQP_URL: amqp://m1pex-rabbitmq:5672
      PORT: 10101
    volumes:
      - ./server:/app:cached
      - /app/node_modules
    ports:
      - "10101:10101"
    depends_on:
      - m1pex-rabbitmq
    restart: on-failure
    command: npm run start
    networks:
      - m1pex-network

  m1pex-client:
    build:
        context: ./client
    working_dir: /app
    environment:
      PORT: 3050
      REACT_APP_WS_PORT: 10101
      REACT_APP_WS_URL: ws://m1pex-server:10101
    volumes:
      - ./client:/app:cached
      - /app/node_modules
    ports:
        - "3050:3050"
    depends_on:
      - m1pex-server
    restart: on-failure
    command: npm run start
    networks:
      - m1pex-network

  m1pex-rabbitmq:
    image: rabbitmq:3-management
    ports:
      - "8080:15672"
      - "5672:5672"
    restart: always
    networks:
      - m1pex-network
...

(client) app.js:

function App() {
  return (
    <ChakraProvider>
      <Content />
    </ChakraProvider>
  );
}

export default App;

function Content() {
  const [message, setMessage] = React.useState('');
  const handleMessageChange = (event) => {
    setMessage(event.target.value);
  };
  const [name, setName] = React.useState(username);
  const handleNameChange = (event) => {
    username = event.target.value;
    setName(event.target.value);
  };

  const [messageLog, setMessageLog] = React.useState([]);

  const { sendMessage, readyState } = useWebSocket(process.env.REACT_APP_WS_URL, {
    onOpen: () => {
      console.log('WebSocket connection opened');
    },
    onClose: () => {
      console.log('WebSocket connection closed');
    },
    onError: (error) => {
      console.error('WebSocket error:', error);
    },
    onMessage: (message) => {
      const msg = JSON.parse(message.data);
      if (msg.message !== null && msg.message !== '') {
        setMessageLog((prevMessageLog) => [...prevMessageLog, msg]);
      }
    },
  });

  const sendMessageToEveryone = () => {
    console.log('WebSocket URL:', process.env.REACT_APP_WS_URL);
    console.log('WebSocket readyState:', readyState);
    sendMessage(JSON.stringify({ username: name, message }));
  };

  return (.....)

(server) index.js :

...
wss.on('connection', (ws) => {
    ws.id = Math.random() * 100000000;
    ws.on('headers', (headers) => {
        headers['Access-Control-Allow-Origin'] = '*';
    });
    ws.on('message', (message) => {
        console.log('received: %s (%i)\n', message, ws.id)
        channel.publish(exchangeName, '', message)
    })
    ws.on('close', () => {
        console.log(`Client disconnected`)
    })

    if (channel !== null) {
        const queueName = `chat-client-${ws.id}`
        channel
            .assertQueue(queueName, {
                autoDelete: true,
                durable: false,
            })
            .then((ok) => {
                channel.bindQueue(queueName, exchangeName, '')
            })
            .then((ok) => {
                channel.consume(queueName, (message) => {
                    ws.send(JSON.stringify(JSON.parse(message.content)))
                })
            })
    }
    console.log(`connection started with ID ${ws.id}`)
})
...

network configuration:

[
  {
    "Name": "m1pex_tp_rabbitmq_m1pex-network",
    "Id": "77c2126b6a6f319d26a14f0c8db19fd5c2b04d7932290c029a389a5af812785e",
    "Created": "2024-06-07T11:45:09.111659472+02:00",
    "Scope": "local",
    "Driver": "bridge",
    "EnableIPv6": false,
    "IPAM": {
      "Driver": "default",
      "Options": null,
      "Config": [
        {
          "Subnet": "192.168.186.0/24",
          "Gateway": "192.168.186.1"
        }
      ]
    },
    "Internal": false,
    "Attachable": false,
    "Ingress": false,
    "ConfigFrom": {
      "Network": ""
    },
    "ConfigOnly": false,
    "Containers": {
      "4637c18b4392941ada7832b5e4656585e1e50ceddf23fe524e445eca6076a7f5": {
        "Name": "m1pex_tp_rabbitmq-m1pex-client-1",
        "EndpointID": "e40ce9820eaa7890c362a919e09f3168278f79aae71950d2dd7b587a9f70bf66",
        "MacAddress": "02:42:c0:a8:ba:04",
        "IPv4Address": "192.168.186.4/24",
        "IPv6Address": ""
      },
      "723afde52e1bdefab9184dccf420d3784d2a88604eb58c75296cd813cc7a45c8": {
        "Name": "some-rabbit",
        "EndpointID": "ef8a678a0c93db75f6f6d6ad085c6fcdd0e90642b9afd13a80e593955bb1caa5",
        "MacAddress": "02:42:c0:a8:ba:02",
        "IPv4Address": "192.168.186.2/24",
        "IPv6Address": ""
      },
      "cb1c29c14e70fb24d99e4efbf41c9e2ad76f1b504fca53a02079ad0cf63d2bef": {
        "Name": "m1pex_tp_rabbitmq-m1pex-server-1",
        "EndpointID": "00f0d0106e8ab0be20b9018c51ca5b6f036f8408b20794f35c89f1fe4a6c123a",
        "MacAddress": "02:42:c0:a8:ba:03",
        "IPv4Address": "192.168.186.3/24",
        "IPv6Address": ""
      }
    },
    "Options": {},
    "Labels": {
      "com.docker.compose.network": "m1pex-network",
      "com.docker.compose.project": "m

hopefully I didn't forget anything, if you need any more informations, the Github repository is right here


Solution

  • You can only use the service name as a host name from containers on the docker bridge network.

    Your browser runs outside the docker network, so from there you have to use the host's IP address and the mapped port. That means that your URL should be ws://localhost:10101/ if you're running your application in a browser on the host machine.

    Unfortunately, you've cut off the error message you get in your screenshot. If you show the full error message, it should say that it can't resolve the m1pex-server host name.