Django channels: websocket connection failed to connect to URL

Hello!

I have created a one-to-one chat application which works as it should in my local server but not in the production server (Heroku).

The messaging app works as follow:

  1. In a template, I have rendered all the room message (from a Room model instance) using a simple view. Each room message are related to all the messages between the two users via a foreign key. So I have a Room model and a Messages model. Also, the unique identifier for each room is a UUID field.
  2. When I click on a room (in the template), the room messages for that specific room is loaded using an AJAX call (similar to Facebook messenger).
  3. At the same time, a web socket connection is created and the two users can communicate in real time.

I have tested this in my local server and the messages are loaded instantly when they are sent.

The issue I have is that in production, when I click on a room message, I see this error in the console:

WebSocket connection to 'wss://www.mydomain.com/meddelanden/18c74633-788f-467f-8134-7a14b1741575/' failed:

Could someone please explain to me what can/could cause this issue?

I have installed and added daphne at the top of the INSTALLED_APPS list, right above whitenoise.

INSTALLED_APPS = [
    "daphne",
    "whitenoise.runserver_nostatic",
    "storages",
    "anymail",
    "channels",
    .
    .
    .
    .
]

My asgi if:

    {
        "http" : get_asgi_application() , 
        "websocket" : AuthMiddlewareStack(
            URLRouter(
                routing.websocket_urlpatterns
            )    
        )
    }
)

My routing is:

websocket_urlpatterns = [
    re_path(r'^meddelanden/(?P<chat_room_uuid>[0-9a-f-]+)/$', OneToOneChatConsumer.as_asgi(), name="onetoonechat"),
]

My consumer class is:

class OneToOneChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        self.chat_room_uuid = self.scope['url_route']['kwargs']['chat_room_uuid'] # Find the room id from the connection scope

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

    async def receive(self, text_data):
        text_data_json = json.loads(text_data)        
        message = text_data_json["message"].strip()
        url_UUID = text_data_json["url_UUID"]
        sender_pk = text_data_json["sender_pk"]

        # The message sender. Changes dynamically depending on who send messages
        sender = await self.sender(sender_pk)
        # sender and reeciver info from the Chatroom instance
        chat_data = await self.chatRoomData()
        
        chat_room_sender = chat_data["sender"]
        chat_room_receiver = chat_data["receiver"]
        
        # Register the message in the database asynchronously
        if sender == chat_room_sender:
            await self.register_message(message, url_UUID, sender=chat_room_sender, receiver=chat_room_receiver)
        else:
            await self.register_message(message, url_UUID, sender=chat_room_receiver, receiver=chat_room_sender)

        await self.channel_layer.group_send(
            self.chat_room_uuid,
            {
                'type': 'send_message',
                'message': message,
            }
        )        
    
    async def disconnect(self, close_code):
        await self.channel_layer.group_discard(self.chat_room_uuid, self.channel_name)
        
    async def send_message(self, event):
        message = event['message']
        await self.send(text_data=json.dumps({
            'message': message,
            "room_data": await self.chatroom_meta_data(self.chat_room_uuid)
        }))
    
    
    @database_sync_to_async
    def sender(self, sender):
        return Member.objects.get(pk=sender)
    
    @database_sync_to_async
    def chatRoomData(self):
        room = get_object_or_404(Chatroom, uuid_field=self.chat_room_uuid)
        chat_room_data = {"sender": room.sender, "receiver": room.receiver}
        return chat_room_data
    
    @database_sync_to_async
    def register_message(self, message, url_UUID, sender, receiver):
        chat_room = Chatroom.objects.get(uuid_field=url_UUID)
        ChatMessage.objects.create(room=chat_room, message=message, sender=sender, receiver=receiver)
    
    @database_sync_to_async
    def chatroom_meta_data(self, room_uuid):
        room = get_object_or_404(Chatroom, uuid_field=room_uuid)
        chat_message = ChatMessage.objects.filter(room=room).latest("timestamp")
        room_data = {"main_user_sender": chat_message.sender.pk,
                    "secondary_user_receiver": chat_message.receiver.pk,
                    "message_sender_username": chat_message.sender.username,
                    "auth_user_id": self.scope["user"].pk,
                    "message_timestamp": chat_message.timestamp.isoformat()}
        
        return room_data

And the Javascript that handles the client side is:

class DjangoChatRoomChannels {
    constructor(chatInputSelector, chatMessageSubmitSelector, messagePreviewSelector, csrftoken) {
        this.chatInput = document.getElementById(chatInputSelector);
        this.chatMessageSubmit = document.querySelector(chatMessageSubmitSelector);
        this.messagePreview = document.querySelectorAll(messagePreviewSelector);
        this.csrftoken = csrftoken;

        this.setupEventListeners();
    }

    // EventListener to setup the websocket connection
    setupEventListeners() {
        if (this.messagePreview) {
            this.messagePreview.forEach((btn) => {
                btn.addEventListener("click", (e) => {
                    e.preventDefault();
                    const urlUUID = btn.dataset.uuidfield;
                    const url = btn.dataset.url;
                    this.setups(urlUUID, url);
                })
            })
        }
    }

    // Opening up a websocket connection
    setups(urlUUID, url) {
        const websocketProtocol = window.location.protocol === "https:" ? "wss" : "ws";
        const wsEndpoint = `${websocketProtocol}://${window.location.host}/meddelanden/${urlUUID}/`;

        this.chatSocket = new WebSocket(wsEndpoint);

        // Websocket message event listener
        this.chatSocket.onmessage = async (e) => {
            e.preventDefault();
            const data = JSON.parse(e.data) // Response from the consumer
            const text_message = data.message;
            const auth_user_id = data.room_data.auth_user_id;
            const main_user_sender = data.room_data.main_user_sender;
            const message_sender_username = data.room_data.message_sender_username;
            const timestamp = new Date(data.room_data.message_timestamp).toLocaleString([], {
                hour: '2-digit',
                minute: '2-digit',
            });

            this.displayMessages(
                auth_user_id, 
                main_user_sender, 
                message_sender_username,
                text_message,
                timestamp,)
        };

        // Websocket close event listener
        this.chatSocket.onclose = (e) => {
            console.log("Chat socket closed unexpectedly");
        };

        // Input keydown event listener
        this.chatInput.focus();
        this.chatInput.onkeydown = (e) => {
            if (e.key === "Enter") {
                e.preventDefault();
                this.chatMessageSubmit.click();
            }
        };

        // Submit the message to the server
        this.chatMessageSubmit.onclick = (e) => {
            e.preventDefault();
            const message = this.chatInput.value.trim();
            const sender = this.chatMessageSubmit.dataset.sender;
            const messageContentScroll = document.getElementById("msgContentscrollDown");

            if (message !== "") {
                this.chatSocket.send(JSON.stringify({
                    "message": message,
                    "url_UUID": urlUUID,
                    "sender_pk": sender,
                }));
            }
            this.chatInput.value = "";

            // Scroll to the bottom of the messages container
            if (messageContentScroll) {
                scrollToBottom(messageContentScroll);
            } else {
                console.warn("Scroll container not found.")
            }
        };
    }

    // Display the sent messages
    displayMessages(auth_user_id, main_user_sender, message_sender_username, text_message, timestamp) {
        let messagesContainer;
        if (screen.width < 768) {
            messagesContainer = document.querySelector(".message_fields__mobile_view");
        } else {
            messagesContainer = document.querySelector(".message_fields__desktop_view");
        }

        if (!messagesContainer) {
            console.log("Message container not found.")
        }

        let messageHTML = '';

        if (auth_user_id == main_user_sender) {
            messageHTML = `
                    <div class="message-box message-sender">
                        <div class="message-box__content">
                            <small>Du</small>
                            <span>${text_message}</span>
                        </div>
                        <small class="message-box__message-timestamp">${timestamp}</small>
                    </div>
                `;
        } else {
            messageHTML = `
                    <div class="message-box message-receiver">
                        <div class="message-box__content">
                            <small>${message_sender_username}</small>
                            <span>${text_message}</span>
                        </div>
                        <small class="message-box__message-timestamp">${timestamp}</small>
                    </div>
                `;
        }

        const newMessageDiv = document.createElement('div');
        newMessageDiv.innerHTML = messageHTML;
        messagesContainer.appendChild(newMessageDiv);
    }

}

if (document.querySelector(".message-page")) {    
    if (screen.width < 768) {
        new DjangoChatRoomChannels("chat-input__mobile", ".chat-message-submit__mobile", ".message-list__item-preview", csrftoken);
    }
    else {
        new DjangoChatRoomChannels("chat-input__desktop", ".chat-message-submit__desktop", ".message-list__item-preview", csrftoken);
    } 
}

Channel layers:

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [env("REDIS_CLOUD_URL")],
        },
    },
}

Did you check any logs?

Yes, I did check the Heroku logs as well. This is what I see:

2024-12-12T23:57:21.517189+00:00 app[web.1]: 10.1.37.165 - - [12/Dec/2024:23:57:21 +0000] "GET /static/scss/custom.min.css.map HTTP/1.1" 200 13221 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
2024-12-12T23:57:28.983921+00:00 heroku[router]: at=info method=GET path="/meddelanden/websocket/18c74633-788f-467f-8134-7a14b1741575/" host=www.mydomain.com request_id=c35ac0a4-b83c-41e5-8184-77ec00934c28 fwd="239.211.139.12" dyno=web.1 connect=0ms service=18ms status=200 bytes=827 protocol=https
2024-12-12T23:57:29.049716+00:00 heroku[router]: at=info method=GET path="/meddelanden/18c74633-788f-467f-8134-7a14b1741575/" host=www.mydomain.com request_id=551b8284-2b4a-4ebf-a483-9f719765665e fwd="239.211.139.12" dyno=web.1 connect=0ms service=8ms status=404 bytes=12602 protocol=https
2024-12-12T23:57:28.983760+00:00 app[web.1]: 10.1.37.165 - - [12/Dec/2024:23:57:28 +0000] "GET /meddelanden/websocket/18c74633-788f-467f-8134-7a14b1741575/ HTTP/1.1" 200 467 "https://www.mydomain.com/meddelanden/inkorg/testuser" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
2024-12-12T23:57:29.049462+00:00 app[web.1]: 10.1.40.149 - - [12/Dec/2024:23:57:29 +0000] "GET /meddelanden/18c74633-788f-467f-8134-7a14b1741575/ HTTP/1.1" 404 12082 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"

Any ideas what’s wrong?

In your routing.py you have: r'^meddelanden/(?P<chat_room_uuid>[0-9a-f-]+)/$'
The actual request path includes “websocket” in it, but your routing pattern doesn’t. This URL mismatch could be causing the connection failure.

Hello!

I managed to solve the issue. It was a quite dumb one.

So there were two problems which I had missed.

Problem 1:

No defining daphne in my Procfile. Previously I had gunicorn set in the Procfile. After tons of reading I understood the difference between gunicorn and daphne.

I changed gunicorn to daphne in order to handle asynchronous code:

web: daphne -b 0.0.0.0 -p $PORT myproject_project.asgi:application

Problem 2:

My Redis database was deleted due to inactivity. I did not realize this until today. This is something that the cloud provide do, but I did not know.

Everything else is as it should be.

Thanks again for your time!