72mins
72mins

Reputation: 90

Websocket connection failed in production. (Nginx, Gunicorn, Daphne) (Django/React)

I am trying to deploy my Django Rest Framework application to production. I have my own server running Debian. I am not new to deploying DRF and React applications and the WSGI part of the application works fine with Gunicorn. The problem I can't solve is I cannot connect to my Websocket from Django Channels no matter what I do.

For further information, running python manage.py runserver and running everything locally works, I normally connect to my websocket.

My routing.py file:

from channels.routing import ProtocolTypeRouter, URLRouter
from django.urls import path, re_path

from apps.chat_app.consumers import ChatConsumer

websocket_urlpatterns = [
    path('ws/chat/<int:id>/<int:curr>/', ChatConsumer.as_asgi()),
]

application = ProtocolTypeRouter({
    'websocket':
        URLRouter(
            websocket_urlpatterns
        )
    ,
})

My consumers file:

import json

from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncWebsocketConsumer
from django.contrib.auth import get_user_model

from apps.chat_app.models import Message


class ChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        current_user_id = self.scope['url_route']['kwargs']['curr']
        other_user_id = self.scope['url_route']['kwargs']['id']

        self.room_name = (
            f'{current_user_id}_{other_user_id}'
            if int(current_user_id) > int(other_user_id)
            else f'{other_user_id}_{current_user_id}'
        )

        self.room_group_name = f'chat_{self.room_name}'

        await self.channel_layer.group_add(self.room_group_name, self.channel_name)
        await self.accept()

    async def disconnect(self, close_code):
        await self.channel_layer.group_discard(self.room_group_name, self.channel_layer)
        await self.disconnect(close_code)

    async def receive(self, text_data=None, bytes_data=None):
        data = json.loads(text_data)
        message = data.get('message', '')
        sender_username = data['sender'].replace('"', '')
        sender = await self.get_user(username=sender_username)

        typing = data.get('typing', False)
        delete = data.get('delete', '')

        if typing:
            await self.channel_layer.group_send(
                self.room_group_name,
                {
                    'type': 'user_typing',
                    'sender': sender_username,
                    'msg': f'{sender.first_name.capitalize()} {sender.last_name.capitalize()} is typing...',
                }
            )
        elif delete:
            await self.delete_message(msg_id=data['delete'])

            await self.channel_layer.group_send(
                self.room_group_name,
                {
                    'type': 'message_delete',
                    'msg_id': data['delete'],
                }
            )
        else:
            await self.channel_layer.group_send(
                self.room_group_name,
                {
                    'type': 'user_typing',
                    'sender': sender_username,
                    'msg': '',
                }
            )

            if message:
                msg = await self.save_message(sender=sender, message=message, thread_name=self.room_group_name)

                await self.channel_layer.group_send(
                    self.room_group_name,
                    {
                        'type': 'chat_message',
                        'msg_id': msg.id,
                        'message': message,
                        'sender': sender_username,
                        'timestamp': msg.timestamp.strftime('%d/%m/%Y %H:%M'),
                        'full_name': f'{sender.first_name.capitalize()} {sender.last_name.capitalize()}',
                    },
                )

    async def message_delete(self, event):
        msg_id = event['msg_id']

        await self.send(
            text_data=json.dumps(
                {
                    'delete': msg_id,
                }
            )
        )

    async def user_typing(self, event):
        username = event['sender']
        msg = event['msg']

        await self.send(
            text_data=json.dumps(
                {
                    'is_typing': True,
                    'sender': username,
                    'msg': msg,
                }
            )
        )

    async def chat_message(self, event):
        message = event['message']
        username = event['sender']
        full_name = event['full_name']
        msg_id = event['msg_id']
        timestamp = event['timestamp']
        typing = event.get('typing', False)
        delete = event.get('delete', '')

        if typing:
            await self.send(
                text_data=json.dumps(
                    {
                        'sender': username,
                        'typing': typing,
                    }
                )
            )
        elif delete:
            await self.send(
                text_data=json.dumps(
                    {
                        'delete': delete,
                    }
                )
            )
        else:
            if message:
                await self.send(
                    text_data=json.dumps(
                        {
                            'msg_id': msg_id,
                            'message': message,
                            'timestamp': timestamp,
                            'sender': username,
                            'full_name': full_name,
                        }
                    )
                )

    @database_sync_to_async
    def get_user(self, username):
        return get_user_model().objects.filter(username=username).first()

    @database_sync_to_async
    def save_message(self, sender, message, thread_name):
        return Message.objects.create(sender=sender, message=message, thread_name=thread_name)

    @database_sync_to_async
    def delete_message(self, msg_id):
        Message.objects.filter(id=msg_id).delete()

My asgi.py file:

import os
from django.core.asgi import get_asgi_application

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator


os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'inp_proj.settings')
django_asgi_app = get_asgi_application()

import apps.chat_app.routing

application = ProtocolTypeRouter(
    {
        'http': django_asgi_app,
        'websocket': AllowedHostsOriginValidator(
            AuthMiddlewareStack(URLRouter(apps.chat_app.routing.websocket_urlpatterns))),
    }
)

My daphne.service file:

[Unit]
Description=WebSocket Daphne Service
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=/www/projectdir
ExecStart=/www/projectdir/venv/bin/python /www/projectdir/venv/bin/daphne -b 0.0.0.0 -p 8001 proj.asgi:application
Restart=on-failure

[Install]
WantedBy=multi-user.target

My gunicorn.service file:

[Unit]
Description=gunicorn daemon
Requires=gunicorn.socket
After=network.target

[Service]
User=jan
Group=www-data
WorkingDirectory=/www/projectdir
ExecStart=/www/projectdir/venv/bin/gunicorn \
          --access-logfile - \
          --workers 3 \
          --bind unix:/run/gunicorn.sock \
          proj.wsgi:application

[Install]
WantedBy=multi-user.target

My gunicorn.socket file:

[Unit]
Description=gunicorn socket

[Socket]
ListenStream=/run/gunicorn.sock

[Install]
WantedBy=sockets.target

And finally, my nginx configuration file:

upstream websocket {
    server 127.0.0.1:8001;
}

server {
    server_name 127.0.0.1 mydomain;

    location = /favicon.ico { access_log off; log_not_found off; }
    location /static/ {
        root /www/projdir;
    }
    
     location / {
        include proxy_params;
        proxy_pass http://unix:/run/gunicorn.sock;
    }
    
    location /ws/ {
    proxy_pass http://websocket;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_redirect off;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Host $server_name;    
    }
}

All of the services (gunicorn socket, gunicorn service, daphne, nginx) work normally and are up and running. The gunicorn WSGI part works fine and the whole application works normally, everything works except I cannot connect to my websocket. This is how I connect to the websocket in my client code:

    const client = useMemo(() => {
        return new w3cwebsocket(`ws://mydomain:8001/ws/chat/${id}/${userId}/`);
    }, [id, userId]);

Also, instead of the mydomain:8001 i tried putting in [serveripv4address]:8001, I tried it without port 8001, I tried both wss and ws even though it is HTTP. Also in my allowed hosts I allowed the domains and even the server ipv4 address.

I tried literally everything I can think of and every post I saw. My Nginx, gunicorn or Daphne don't show any errors.

Upvotes: 0

Views: 1025

Answers (1)

Gracen Ownby
Gracen Ownby

Reputation: 474

You should show/check the javascript error you are seeing on the page in developer tools > console.

However, I can see here that you would not be able to connect over the internet because you are trying to connect via ws, its probably throwing a HTTPDOMException or some type of connection failed. You need to connect to a live server on the internet using wss.

The following is an apache server configuration which I hope you should be able to translate to nginx. The idea is that our daphne service is actually running our python applications local to the server. Whereas your daphne command specifies a asgi.py file, our server configuration also needs to specify a path to access this websocket application running locally. We need to redirect wss to ws. For example:

RewriteEngine on
RewriteCond %{HTTP:UPGRADE} ^WebSocket$ [NC,OR]
RewriteCond %{HTTP:CONNECTION} ^Upgrade$ [NC]
RewriteRule .* ws://127.0.0.1:8001%{REQUEST_URI} [P,QSA,L]
ProxyPass /wss/ wss://127.0.0.1:8001/
ProxyPassReverse /wss/ wss://127.0.0.1:8001/
...
SSLEngine on
SSLCertificateFile /etc/ssl/certificate.crt
SSLCertificateKeyFile /etc/ssl/private/private.key
SSLCertificateChainFile /etc/ssl/ca_bundle.crt

In production you also need an SSL certification which to ensure an encrypted connection, and you will need to specify that in your daphne instruction.

daphne -b 0.0.0.0 -p 8001 django_project.asgi:application // Local Development Level
daphne -e ssl:443:privateKey=key.pem:certKey=crt.pem django_project.asgi:application // Production Level

Upvotes: 0

Related Questions