简体   繁体   中英

Websockets with Django Channels on Heroku

I am trying to deploy my app to heroku. The app has a simple chatting system that uses Websockets and django channels.

When I test my app using python manage.py runserver the app behaves just as intended.

I tried deploying the app and all features work except for the chatting system.

Here is the error message I am getting in the Chrome Console:

layout.js:108 Mixed Content: The page at 'https://desolate-lowlands-74512.herokuapp.com/index' was loaded over HTTPS, but attempted to connect to the insecure WebSocket endpoint 'ws://desolate-lowlands-74512.herokuapp.com/ws/chat/19/'. This request has been blocked; this endpoint must be available over WSS.

This is what I tried to fix it: I went from ws to wss I changed this:

 const chatSocket = new WebSocket(
        'ws://'
        + window.location.host
        + '/ws/chat/'
        + friendship_id
        + '/'
      );
      console.log(chatSocket)

to this:

 const chatSocket = new WebSocket(
        'wss://'
        + window.location.host
        + '/ws/chat/'
        + friendship_id
        + '/'
      );
      console.log(chatSocket)

with this change the websocket loads but the chatting system still doesn't work. The messages still don't get sent or received

This is the error message I get when opening the Chatbox:

layout.js:108 WebSocket connection to 'wss://desolate-lowlands-74512.herokuapp.com/ws/chat/19/' failed: Error during WebSocket handshake: Unexpected response code: 404

and this is the error message I get when I try to send the message:

WebSocket is already in CLOSING or CLOSED state.

Here is the code: This is my asgi.py file:

"""
ASGI config for DBSF project.

It exposes the ASGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
"""

import os

from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
import social.routing

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'DBSF.settings')

application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    "websocket": AuthMiddlewareStack(
        URLRouter(
            social.routing.websocket_urlpatterns
        )
    ),
})

here is routing.py

from django.urls import re_path
from . import consumers

websocket_urlpatterns = [
    re_path(r'ws/chat/(?P<friendship_id>\w+)/$', consumers.ChatConsumer.as_asgi()),
]

Here's consumers.py

import json
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer
from .models import Message, Friendship, User
import datetime

class ChatConsumer(WebsocketConsumer):
   
    def connect(self):
        self.room_name = self.scope['url_route']['kwargs']['friendship_id']
        self.room_group_name = 'chat_%s' % self.room_name
        # Join room group
        async_to_sync(self.channel_layer.group_add)(
            self.room_group_name,
            self.channel_name
        )

        self.accept()

    def disconnect(self, close_code):
        # Leave room group
        async_to_sync(self.channel_layer.group_discard)(
            self.room_group_name,
            self.channel_name
        )

    # Receive message from WebSocket
    def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json['message']
        sender = text_data_json['sender']
        receiver = text_data_json['receiver']
        friendship_id = self.scope['url_route']['kwargs']['friendship_id']
        message_to_save = Message(conversation=Friendship.objects.get(id=friendship_id), sender=User.objects.get(username=sender), receiver=User.objects.get(username=receiver), text=message, date_sent=datetime.datetime.now())
        message_to_save.save()

        # Send message to room group
        async_to_sync(self.channel_layer.group_send)(
            self.room_group_name,
            {
                'type': 'chat_message',
                'message': message,
                'sender': sender,
                'receiver': receiver,
                'id': message_to_save.id
            }
        )

    # Receive message from room group
    def chat_message(self, event):
        message = event['message']
        sender = event['sender']
        receiver = event['receiver']
        id = event['id']

        # Send message to WebSocket
        self.send(text_data=json.dumps({
            'message': message,
            'sender': sender,
            'receiver': receiver,
            'id': id,
        }))

Here is settings.py

"""
Django settings for DBSF project.

Generated by 'django-admin startproject' using Django 3.1.2.

For more information on this file, see
https://docs.djangoproject.com/en/3.1/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/ref/settings/
"""

from pathlib import Path
import django_heroku
import os
from dotenv import load_dotenv
load_dotenv()
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ['SECRET_KEY']
AUTH_USER_MODEL = 'social.User'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ['desolate-lowlands-74512.herokuapp.com', 'localhost', '127.0.0.1']

# Application definition

INSTALLED_APPS = [
    'channels',
    'social',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'DBSF.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

REST_FRAMEWORK = {
    # Use Django's standard `django.contrib.auth` permissions,
    # or allow read-only access for unauthenticated users.
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
    ]
}

WSGI_APPLICATION = 'DBSF.wsgi.application'
ASGI_APPLICATION = 'DBSF.asgi.application'
CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('127.0.0.1', 6379)],
        },
    },
}

# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}


# Password validation
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/3.1/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'EST'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATIC_URL = '/static/'
MEDIA_ROOT= os.path.join(BASE_DIR, 'media/')
MEDIA_URL= "/media/"

here's wsgi.py

"""
WSGI config for DBSF project.

It exposes the WSGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
"""

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'DBSF.settings')

application = get_wsgi_application()

I assume the once I change from ws to wss the consumer can't connect. I think this will be an easy fix but I can't figure out what code i need to change. I suspect that it is something in asgy.py or routing.py

Please let me know if it is unclear what I am asking or if I need to show any other files

Question details

Hello, here is some steps that I find to myself to deploy similar mysite app for heroku server. I used this libraries:

django==3.2.6
channels==3.0.4
python==3.9.6

Channels library

Quick start

You can read how to work with it at Tutorial . First of all, follow the installation and tutorial.

In your Procfile you need to use daphne which is installed with channels library by default:

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

You don't need any workers if you use latest version of channels.

At heroku environment $PORT replace with some port that it provide. You can provide it locally by using .env file.

The last thing you need to replace ws:// with wss:// (see Errors and solutions) at room.html .

Errors and solutions

Unexpected response code: 200 (or other code XXX) (solved):

  • Be sure you include your application and channels via settings ( mysite.settings ) and use asgi application:
INSTALLED_APPS = [
    'channels',
    'chat',
    ...
]
...
ASGI_APPLICATION = "mysite.asgi.application"
  • Be sure you use channel layers ( mysite.settings ).
CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels.layers.InMemoryChannelLayer',
    },
}

According Documentation you should use database for production, but for local environment you may use channels.layers.InMemoryChannelLayer .

  • Be sure you run asgi server (not wsgi ) because you need asynchronous behaviour. Also, for deployment you should use daphne instead of gunicorn . daphne is included in channels library by default, so you don't need to install it manually. Basic run server will look like (Terminal):
daphne -b 0.0.0.0 -p $PORT mysite.asgi:application

where $PORT is specific port (for UNIX system is 5000). (That format used for heroku application, you can change it manually).

Error in connection establishment: net::ERR_SSL_PROTOCOL_ERROR and same errors with using https connection (solved):

Difference between ws and wss?

You may think about to use your server via wss protocol: replace ws://... with wss://... or use following template in your html ( chat/templates/chat/room.html ):

(window.location.protocol === 'https:' ? 'wss' : 'ws') + '://'

Hope this answer is useful for channels with Django .

It sounds like you are not properly setting the Procfile for your project. Because Channels apps need both a HTTP/WebSocket server and a backend channel consumer, the Procfile needs to define both of these types:

release: python manage.py migrate
web: daphne therapist_portal.asgi:application --port $PORT --bind 0.0.0.0
worker: python manage.py runworker channel_layer

After you do your initial deploy, you'll need to make sure both process types are running (Heroku will only start the web dyno by default, you can also change the dynos tiers according to your needs):

heroku ps:scale web=1:free worker=1:free

If you are still running into problems, you might want to change your asgy.py file to something like:

import os
import channels
import django

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "therapist_portal.settings")
django.setup()

from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
import social.routing

application = ProtocolTypeRouter({
  "http": get_asgi_application(),
  "websocket": AuthMiddlewareStack(
        URLRouter(
            social.routing.websocket_urlpatterns
        )
    ),
})

This will ensure that Daphne will start up properly.

NOTE 1: I'm assuming you are using Django 3.* and Channels 3.*

NOTE 2: Part of this answer came from the somewhat outdated docs from Heroku.

If you are using channels >=2, then you don't need any worker. All you need is to set your Procfile and settings properly.

Procfile

release: python manage.py migrate
web: daphne <your-app>.asgi:application --port $PORT --bind 0.0.0.0 -v2

settings.py

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [os.environ.get('REDIS_URL')],
        },
    },
}

Notice here that we don't have the extra 6379 port in the hosts above.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM