So my question is:
Has anyone been successful in using Django site + Django channels + Heroku hosting approach for a chat app on their site?
If so, then please post your relevant codes. Thanks.
Here is the live site chat page:
Chat Index (join a room => broken state => this quesiton)
So I am going to show you some code real quick:
asgi.py:
import os
import django
from channels.auth import AuthMiddlewareStack # new import
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
asgi_app = get_asgi_application()
import chat.routing
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'jobs.settings')
application = ProtocolTypeRouter({
'http': asgi_app,
'websocket': AuthMiddlewareStack( # new
URLRouter(
chat.routing.websocket_urlpatterns
)
), # new
})
Procfile (a heroku thing):
web: gunicorn jobs.wsgi --log-file -
worker: python manage.py runworker channels --settings=jobs.settings -v2
chat/urls.py:
from django.urls import path
from . import views
urlpatterns = [
path('', views.index_view, name='chat-index'),
path('<str:room_name>/', views.room_view, name='chat-room'),
]
chat/views.py:
from django.shortcuts import render
import os
from chat.models import Room
def index_view(request):
return render(request, 'chat/index.html', {
'rooms': Room.objects.all(),
})
def room_view(request, room_name):
chat_room, created = Room.objects.get_or_create(name=room_name)
return render(request, 'chat/room.html', {
'room': chat_room,
})
chat/routing.py:
from django.urls import re_path
import chat.consumers
websocket_urlpatterns = [
re_path(r'ws/chat/(?P<room_name>\w+)/$', chat.consumers.ChatConsumer.as_asgi()),
]
chat/models.py:
from accounts.models import User
from django.db import models
class Room(models.Model):
name = models.CharField(max_length=128)
online = models.ManyToManyField(to=User, blank=True)
def get_online_count(self):
return self.online.count()
def join(self, user):
self.online.add(user)
self.save()
def leave(self, user):
self.online.remove(user)
self.save()
def __str__(self):
return f'{self.name} ({self.get_online_count()})'
class Message(models.Model):
user = models.ForeignKey(to=User, on_delete=models.CASCADE)
room = models.ForeignKey(to=Room, on_delete=models.CASCADE)
content = models.CharField(max_length=512)
timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f'{self.user.username}: {self.content} [{self.timestamp}]'
chat/consumers.py:
import json
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer
from .models import Room, Message
class ChatConsumer(WebsocketConsumer):
def __init__(self, *args, **kwargs):
super().__init__(args, kwargs)
self.room_name = None
self.room_group_name = None
self.room = None
self.user = None
self.user_inbox = None
def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = f'chat_{self.room_name}'
self.room = Room.objects.get(name=self.room_name)
self.user = self.scope['user']
self.user_inbox = f'inbox_{self.user.email.replace("@", "_")}'
# connection has to be accepted
self.accept()
# join the room group
async_to_sync(self.channel_layer.group_add)(
self.room_group_name,
self.channel_name,
)
users = [user.email for user in self.room.online.all()]
# send the user list to the newly joined user
self.send(json.dumps({
'type': 'user_list',
'users': users,
}))
if self.user.is_authenticated:
# create a user inbox for private messages
async_to_sync(self.channel_layer.group_add)(
self.user_inbox,
self.channel_name,
)
# send the join event to the room
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'user_join',
'user': self.user.email,
}
)
self.room.online.add(self.user)
def disconnect(self, close_code):
async_to_sync(self.channel_layer.group_discard)(
self.room_group_name,
self.channel_name,
)
if self.user.is_authenticated:
# delete the user inbox for private messages
async_to_sync(self.channel_layer.group_add)(
self.user_inbox,
self.channel_name,
)
# send the leave event to the room
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'user_leave',
'user': self.user.email,
}
)
self.room.online.remove(self.user)
def receive(self, text_data=None, bytes_data=None):
text_data_json = json.loads(text_data)
message = text_data_json['message']
if not self.user.is_authenticated:
return
if message.startswith('/pm '):
split = message.split(' ', 2)
target = split[1]
target_msg = split[2]
# send private message to the target
async_to_sync(self.channel_layer.group_send)(
f'inbox_{target}',
{
'type': 'private_message',
'user': self.user.email,
'message': target_msg,
}
)
# send private message delivered to the user
self.send(json.dumps({
'type': 'private_message_delivered',
'target': target,
'message': target_msg,
}))
return
# send chat message event to the room
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'chat_message',
'user': self.user.email,
'message': message,
}
)
Message.objects.create(user=self.user, room=self.room, content=message)
def chat_message(self, event):
self.send(text_data=json.dumps(event))
def user_join(self, event):
self.send(text_data=json.dumps(event))
def user_leave(self, event):
self.send(text_data=json.dumps(event))
def private_message(self, event):
self.send(text_data=json.dumps(event))
def private_message_delivered(self, event):
self.send(text_data=json.dumps(event))
Errors in the browser dev console:
room.js:1 Sanity check from room.js.
DevTools failed to load source map: Could not load content for chrome-extension://mooikfkahbdckldjjndioackbalphokd/assets/atoms.js.map: System error: net::ERR_BLOCKED_BY_CLIENT
DevTools failed to load source map: Could not load content for chrome-extension://mooikfkahbdckldjjndioackbalphokd/assets/polyfills.js.map: System error: net::ERR_BLOCKED_BY_CLIENT
DevTools failed to load source map: Could not load content for chrome-extension://mooikfkahbdckldjjndioackbalphokd/assets/escape.js.map: System error: net::ERR_BLOCKED_BY_CLIENT
DevTools failed to load source map: Could not load content for chrome-extension://mooikfkahbdckldjjndioackbalphokd/assets/playback.js.map: System error: net::ERR_BLOCKED_BY_CLIENT
DevTools failed to load source map: Could not load content for chrome-extension://mooikfkahbdckldjjndioackbalphokd/assets/record.js.map: System error: net::ERR_BLOCKED_BY_CLIENT
content_start.js:137 Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self'". Either the 'unsafe-inline' keyword, a hash ('sha256-mc9LH2e3kV1BBF1B3icEq0dR656rD3pdHToRTsWEQTI='), or a nonce ('nonce-...') is required to enable inline execution.
syLt @ content_start.js:137
BEWy @ content_start.js:123
YUWZ @ content_start.js:95
jOSM @ content_start.js:155
mKFS @ content_start.js:238
cYUN @ content_start.js:190
Sypg @ content_start.js:212
TxKY @ content_start.js:253
(anonymous) @ content_start.js:259
room.js:48 WebSocket connection to 'wss://fairworkcycle.herokuapp.com/ws/chat/test/' failed:
connect @ room.js:48
(anonymous) @ room.js:104
room.js:99 WebSocket encountered an error: undefined
room.js:100 Closing the socket.
room.js:55 WebSocket connection closed unexpectedly. Trying to reconnect in 2s...
room.js:57 Reconnecting...
room.js:48 WebSocket connection to 'wss://fairworkcycle.herokuapp.com/ws/chat/test/' failed:
connect @ room.js:48
(anonymous) @ room.js:58
room.js:99 WebSocket encountered an error: undefined
room.js:100 Closing the socket.
room.js:55 WebSocket connection closed unexpectedly. Trying to reconnect in 2s...
room.js:57 Reconnecting...
room.js:48 WebSocket connection to 'wss://fairworkcycle.herokuapp.com/ws/chat/test/' failed:
connect @ room.js:48
(anonymous) @ room.js:58
room.js:99 WebSocket encountered an error: undefined
room.js:100 Closing the socket.
room.js:55 WebSocket connection closed unexpectedly. Trying to reconnect in 2s...
room.js:57 Reconnecting...
room.js:48 WebSocket connection to 'wss://fairworkcycle.herokuapp.com/ws/chat/test/' failed:
connect @ room.js:48
(anonymous) @ room.js:58
room.js:99 WebSocket encountered an error: undefined
room.js:100 Closing the socket.
room.js:55 WebSocket connection closed unexpectedly. Trying to reconnect in 2s...
room.js:57 Reconnecting...
Errors on the heroku logs --tail
side:
2022-05-09T00:30:42.032857+00:00 heroku[router]: at=info method=GET path="/ws/chat/test/" host=fairworkcycle.herokuapp.com request_id=e397b517-6063-4f90-bd03-f08eadb9ceef fwd="47.215.230.184" dyno=web.1 connect=0ms service=8ms status=404 bytes=3622 protocol=https
2022-05-09T00:30:45.002477+00:00 heroku[router]: at=info method=GET path="/ws/chat/test/" host=fairworkcycle.herokuapp.com request_id=f7bc7490-c132-4e6f-9808-7ce3d0924c88 fwd="47.215.230.184" dyno=web.1 connect=0ms service=6ms status=404 bytes=3622 protocol=https
2022-05-09T00:30:45.001183+00:00 app[web.1]: 10.1.3.205 - - [09/May/2022:00:30:44 +0000] "GET /ws/chat/test/ HTTP/1.1" 404 3256 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36"
2022-05-09T00:30:48.041544+00:00 heroku[router]: at=info method=GET path="/ws/chat/test/" host=fairworkcycle.herokuapp.com request_id=c49ace87-bd89-4862-aff1-52cb31eda980 fwd="47.215.230.184" dyno=web.1 connect=0ms service=6ms status=404 bytes=3622 protocol=https
2022-05-09T00:30:48.040336+00:00 app[web.1]: 10.1.90.231 - - [09/May/2022:00:30:48 +0000] "GET /ws/chat/test/ HTTP/1.1" 404 3256 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36"
2022-05-09T00:30:51.024792+00:00 app[web.1]: 10.1.16.74 - - [09/May/2022:00:30:51 +0000] "GET /ws/chat/test/ HTTP/1.1" 404 3256 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36"
2022-05-09T00:30:51.024631+00:00 heroku[router]: at=info method=GET path="/ws/chat/test/" host=fairworkcycle.herokuapp.com request_id=38938a87-f83b-4ce4-82cb-785d4eed33c5 fwd="47.215.230.184" dyno=web.1 connect=0ms service=10ms status=404 bytes=3622 protocol=https
2022-05-09T00:30:54.046057+00:00 app[web.1]: 10.1.22.44 - - [09/May/2022:00:30:54 +0000] "GET /ws/chat/test/ HTTP/1.1" 404 3256 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36"
2022-05-09T00:30:54.047688+00:00 heroku[router]: at=info method=GET path="/ws/chat/test/" host=fairworkcycle.herokuapp.com request_id=b3097957-73f6-4d17-9532-c84ba363a30c fwd="47.215.230.184" dyno=web.1 connect=0ms service=6ms status=404 bytes=3622 protocol=https
2022-05-09T00:30:57.069357+00:00 heroku[router]: at=info method=GET path="/ws/chat/test/" host=fairworkcycle.herokuapp.com request_id=abfe6f78-5bbf-47e2-b956-7307db3cf5d1 fwd="47.215.230.184" dyno=web.1 connect=0ms service=7ms status=404 bytes=3622 protocol=https
2022-05-09T00:30:57.068065+00:00 app[web.1]: 10.1.26.127 - - [09/May/2022:00:30:57 +0000] "GET /ws/chat/test/ HTTP/1.1" 404 3256 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36"
2022-05-09T00:31:00.065090+00:00 app[web.1]: 10.1.22.44 - - [09/May/2022:00:31:00 +0000] "GET /ws/chat/test/ HTTP/1.1" 404 3256 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36"
2022-05-09T00:31:00.066342+00:00 heroku[router]: at=info method=GET path="/ws/chat/test/" host=fairworkcycle.herokuapp.com request_id=05545a7c-b356-43af-ae66-499d1648fa74 fwd="47.215.230.184" dyno=web.1 connect=0ms service=10ms status=404 bytes=3622 protocol=https
2022-05-09T00:31:03.078298+00:00 heroku[router]: at=info method=GET path="/ws/chat/test/" host=fairworkcycle.herokuapp.com request_id=50c66355-ce3b-4b1a-ae80-408f31a1e098 fwd="47.215.230.184" dyno=web.1 connect=0ms service=5ms status=404 bytes=3622 protocol=https
2022-05-09T00:31:03.077381+00:00 app[web.1]: 10.1.25.200 - - [09/May/2022:00:31:03 +0000] "GET /ws/chat/test/ HTTP/1.1" 404 3256 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36"
2022-05-09T00:31:06.083485+00:00 heroku[router]: at=info method=GET path="/ws/chat/test/" host=fairworkcycle.herokuapp.com request_id=d0fc6c58-49b8-4682-b660-ef8b06db3f95 fwd="47.215.230.184" dyno=web.1 connect=0ms service=11ms status=404 bytes=3622 protocol=https
2022-05-09T00:31:06.082592+00:00 app[web.1]: 10.1.32.167 - - [09/May/2022:00:31:06 +0000] "GET /ws/chat/test/ HTTP/1.1" 404 3256 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36"
2022-05-09T00:31:09.130023+00:00 heroku[router]: at=info method=GET path="/ws/chat/test/" host=fairworkcycle.herokuapp.com request_id=0b1bcc2d-6cf9-4705-86f4-f338ecf1995d fwd="47.215.230.184" dyno=web.1 connect=0ms service=16ms status=404 bytes=3622 protocol=https
2022-05-09T00:31:09.129169+00:00 app[web.1]: 10.1.25.200 - - [09/May/2022:00:31:09 +0000] "GET /ws/chat/test/ HTTP/1.1" 404 3256 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36"
2022-05-09T00:31:12.113146+00:00 heroku[router]: at=info method=GET path="/ws/chat/test/" host=fairworkcycle.herokuapp.com request_id=584ec6bf-2403-49b8-b6fa-5f39652fa160 fwd="47.215.230.184" dyno=web.1 connect=0ms service=6ms status=404 bytes=3622 protocol=https
2022-05-09T00:31:12.111819+00:00 app[web.1]: 10.1.93.52 - - [09/May/2022:00:31:12 +0000] "GET /ws/chat/test/ HTTP/1.1" 404 3256 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36"
2022-05-09T00:31:15.287083+00:00 app[web.1]: 10.1.32.167 - - [09/May/2022:00:31:15 +0000] "GET /ws/chat/test/ HTTP/1.1" 404 3256 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36"
2022-05-09T00:31:15.288142+00:00 heroku[router]: at=info method=GET path="/ws/chat/test/" host=fairworkcycle.herokuapp.com request_id=8e8a0019-728b-4bd9-b87b-4980e0769459 fwd="47.215.230.184" dyno=web.1 connect=0ms service=4ms status=404 bytes=3622 protocol=https
2022-05-09T00:31:18.372489+00:00 heroku[router]: at=info method=GET path="/ws/chat/test/" host=fairworkcycle.herokuapp.com request_id=dd5962a9-85b9-40e1-9275-59f04af63b98 fwd="47.215.230.184" dyno=web.1 connect=0ms service=4ms status=404 bytes=3622 protocol=https
2022-05-09T00:31:18.371198+00:00 app[web.1]: 10.1.43.209 - - [09/May/2022:00:31:18 +0000] "GET /ws/chat/test/ HTTP/1.1" 404 3256 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36"
These both go on indefinitely (it’s trying to make a connection, but cant). That’s with the Redis addon installed onto fairworkcycle.herokuapp.com
. However, with InMemoryStore on local machine, everything works great.
Here’s the relevant part of settings.py
:
# Chat settings
ASGI_APPLICATION = "jobs.asgi.application"
if os.environ.get('LIVE_SITE', 1) == '0':
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels.layers.InMemoryChannelLayer"
}
}
else:
CHANNEL_LAYERS = {
"default": {
"BACKEND": "asgi_redis.RedisChannelLayer",
"CONFIG": {
"hosts": [os.environ.get('REDIS_URL', 'redis://localhost:6379')],
},
"ROUTING": "chat.routing.channel_routing",
},
}
The Frontend
chat/room.html:
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<title>django-channels-chat</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.min.js"></script>
<style>
#chatLog {
height: 300px;
background-color: #FFFFFF;
resize: none;
}
#onlineUsersSelector {
height: 300px;
}
</style>
</head>
<body>
<div class="container mt-3 p-5">
<h2>django-channels-chat</h2>
<div class="row">
<div class="col-12 col-md-8">
<div class="mb-2">
<label for="chatLog">Room: #{{ room.name }}</label>
<textarea class="form-control" id="chatLog" readonly></textarea>
</div>
<div class="input-group">
<input type="text" class="form-control" id="chatMessageInput" placeholder="Enter your chat message">
<div class="input-group-append">
<button class="btn btn-success" id="chatMessageSend" type="button">Send</button>
</div>
</div>
</div>
<div class="col-12 col-md-4">
<label for="onlineUsers">Online users</label>
<select multiple class="form-control" id="onlineUsersSelector">
</select>
</div>
</div>
{{ room.name|json_script:"roomName" }}
</div>
<script src="{% static 'js/chat/room.js' %}"></script>
</body>
</html>
js/chat/room.js
console.log("Sanity check from room.js.");
const roomName = JSON.parse(document.getElementById('roomName').textContent);
let chatLog = document.querySelector("#chatLog");
let chatMessageInput = document.querySelector("#chatMessageInput");
let chatMessageSend = document.querySelector("#chatMessageSend");
let onlineUsersSelector = document.querySelector("#onlineUsersSelector");
// adds a new option to 'onlineUsersSelector'
function onlineUsersSelectorAdd(value) {
if (document.querySelector("option[value='" + value + "']")) return;
let newOption = document.createElement("option");
newOption.value = value;
newOption.innerHTML = value;
onlineUsersSelector.appendChild(newOption);
}
// removes an option from 'onlineUsersSelector'
function onlineUsersSelectorRemove(value) {
let oldOption = document.querySelector("option[value='" + value + "']");
if (oldOption !== null) oldOption.remove();
}
// focus 'chatMessageInput' when user opens the page
chatMessageInput.focus();
// submit if the user presses the enter key
chatMessageInput.onkeyup = function(e) {
if (e.keyCode === 13) { // enter key
chatMessageSend.click();
}
};
// clear the 'chatMessageInput' and forward the message
chatMessageSend.onclick = function() {
if (chatMessageInput.value.length === 0) return;
chatSocket.send(JSON.stringify({
"message": chatMessageInput.value,
}));
chatMessageInput.value = "";
};
let chatSocket = null;
function connect() {
let protocol = (window.location.protocol === 'https:' ? 'wss' : 'ws') + '://';
chatSocket = new WebSocket(protocol + window.location.host + "/ws/chat/" + roomName + "/");
chatSocket.onopen = function(e) {
console.log("Successfully connected to the WebSocket.");
}
chatSocket.onclose = function(e) {
console.log("WebSocket connection closed unexpectedly. Trying to reconnect in 2s...");
setTimeout(function() {
console.log("Reconnecting...");
connect();
}, 2000);
};
chatSocket.onmessage = function(e) {
const data = JSON.parse(e.data);
console.log(data);
switch (data.type) {
case "chat_message":
chatLog.value += data.user + ": " + data.message + "\n";
break;
case "user_list":
for (let i = 0; i < data.users.length; i++) {
onlineUsersSelectorAdd(data.users[i]);
}
break;
case "user_join":
chatLog.value += data.user + " joined the room.\n";
onlineUsersSelectorAdd(data.user);
break;
case "user_leave":
chatLog.value += data.user + " left the room.\n";
onlineUsersSelectorRemove(data.user);
break;
case "private_message":
chatLog.value += "PM from " + data.user + ": " + data.message + "\n";
break;
case "private_message_delivered":
chatLog.value += "PM to " + data.target + ": " + data.message + "\n";
break;
default:
console.error("Unknown message type!");
break;
}
// scroll 'chatLog' to the bottom
chatLog.scrollTop = chatLog.scrollHeight;
};
chatSocket.onerror = function(err) {
console.log("WebSocket encountered an error: " + err.message);
console.log("Closing the socket.");
chatSocket.close();
}
}
connect();
onlineUsersSelector.onchange = function() {
chatMessageInput.value = "/pm " + onlineUsersSelector.value + " ";
onlineUsersSelector.value = null;
chatMessageInput.focus();
};
What I’ve tried:
About 10 different solutions from Stack overflow. SO got me this far though, so thought it would help me cross this hurdle. I’ve googled for both the frontend and backend error strings.
Next thing I’m going to try is the REDIS TLS environment variable that also auto-appeared on Heroku dashboard after I installed the Redis addon.