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:
- 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.
- 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).
- 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")],
},
},
}