1

I am trying to open a websocket connection from my angular 2+ app with my Django backend using Django Channels. I went through this tutorial: https://www.youtube.com/watch?v=RVH05S1qab8 and managed to get everything working with the javascript portion written inline in a Django html template. But I am having issues simply opening a websocket connection when I migrated the front-end chat form to a separate angular 2 app. Both font-end and backends are hosted locally.

Front End - Chat Form Template

<div *ngFor="let message of thread.messages">
  <div>{{message.message}}</div>
</div>

<form #formData [formGroup]="form">
  <div class="form-group">
    <input formControlName="messageInput" #msgInput type="text" class="form-control" id="newMessage" placeholder="Type a message">
  </div>
  <button (click)="onSubmit($event)" type="submit" class="btn btn-primary">Send</button>
</form>
</div>

Front End - Chat Form Component

  otherUser: User;
  me: User;
  threadId: number;
  thread: Thread;
  endpoint: string;
  socket: WebSocket;

  form = new FormGroup({
    messageInput: new FormControl("")
  });

  get messageInput() {
    return this.form.get('messageInput');
  }


  getThreadById(id: number) {
    this._chatService.getThreadById(id).subscribe(thread => {
      console.log("response: thread found", thread);
      this.thread = thread;
    },
      error => {
        console.log("error", error);
      });
  }

  getEndpoint() {
    let loc = window.location
    let wsStart = "ws://"
    if (loc.protocol == "https:") {
      wsStart = 'wss://'
    }
    this.endpoint = wsStart + loc.host + loc.pathname;
    return this.endpoint;
  }


  constructor(private _activatedRoute: ActivatedRoute) {
  }

  ngOnInit() {
    this._activatedRoute.params.subscribe(params => { this.threadId = params['id']; });
    if (this.threadId) {
      this.getThreadById(this.threadId);
    }
    this.getEndpoint();
    this.createSocket();
  }

  createSocket() {
    this.socket = new WebSocket(this.endpoint);
    console.log("endpoint ", this.endpoint);

    this.socket.onopen = (e) => {
      console.log("open", e);
    }

    this.socket.onmessage = (e) => {
      console.log("message", e);
      let chatDataMsg = JSON.parse(e.data);
      this.thread.messages.push(chatDataMsg.message);
    }

    this.socket.onerror = (e) => {
      console.log("error", e);
    }

    this.socket.onclose = (e) => {
      console.log("close", e);
    }

  }

  onSubmit($event) {
    let msgText = this.messageInput.value;
    let finalData = {
      'message': msgText
    };
    let chatMessage: ChatMessage = new ChatMessage(this.thread, this.me, new Date(), msgText);
    this.socket.send(JSON.stringify(finalData))
    this.form.reset();
  }

Django Backend Project Settings

ASGI_APPLICATION = "joole.routing.application"

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


ALLOWED_HOSTS = ['joole-api.herokuapp.com', '.herokuapp.com', '127.0.0.1']

Django Websocket Routing

from channels.routing import ProtocolTypeRouter, URLRouter
from django.conf.urls import url
from channels.auth import AuthMiddlewareStack
from channels.security.websocket import AllowedHostsOriginValidator, OriginValidator
from chat.consumers import ChatConsumer

application = ProtocolTypeRouter({
    'websocket': AllowedHostsOriginValidator(
        AuthMiddlewareStack(
            URLRouter(
                [
                    url(r"^messages/(?P<id>[\w.@+-]+)/$", ChatConsumer)
                ]
            )
        )
    )
})

Django Project Urls

from django.urls import path, include
from django.conf import settings

urlpatterns = [
    path('messages/', include('chat.urls')),
]

Django WebSocket Consumer

import asyncio
import json
from users.models import CustomUser, Employee
from channels.consumer import AsyncConsumer
from channels.db import database_sync_to_async
from .models import Thread, ChatMessage


class ChatConsumer(AsyncConsumer):

    async def websocket_connect(self, event):
        print("CONNECTED", event)

        thread_id = self.scope['url_route']['kwargs']['id']
        thread_obj = await self.get_thread(thread_id)
        self.thread_obj = thread_obj
        chat_room = f"thread_{thread_obj.id}"
        self.chat_room = chat_room
        await self.channel_layer.group_add(
            chat_room,
            self.channel_name
        )
        await self.send({
            "type": 'websocket.accept'
        })

    async def websocket_receive(self, event):
        print("receive", event)
        front_text = event.get('text', None)
        if front_text is not None:
            loaded_dict_data = json.loads(front_text)
            msg = loaded_dict_data.get('message')
            user = self.scope['user']
            username = 'default'
            if user.is_authenticated:
                username = user.email
            myResponse = {
                'message': msg,
                'username': user.email
            }
            await self.create_chat_message(user, msg)
            # broadcasts the message event to be sent
            await self.channel_layer.group_send(
                self.chat_room,
                {
                    "type": "chat_message",
                    "text": json.dumps(myResponse)
                }
            )

    async def chat_message(self, event):
        # send the actual message event
        print("message", event)
        await self.send({
            "type": "websocket.send",
            "text": event["text"]
        })

    async def websocket_disconnect(self, event):
        print("disconnected", event)

    @database_sync_to_async
    def get_thread(self, id):
        return Thread.objects.get(id=id)

    @database_sync_to_async
    def create_chat_message(self, me, msg):
        thread_obj = self.thread_obj
        return ChatMessage.objects.create(thread=thread_obj, user=me, message=msg)

Django Chat Models

class Thread(models.Model):
    first = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='chat_thread_first')
    second = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='chat_thread_second')
    updated = models.DateTimeField(auto_now=True)
    timestamp = models.DateTimeField(auto_now_add=True)

    @property
    def room_group_name(self):
        return f'chat_{self.id}'

    def broadcast(self, msg=None):
        if msg is not None:
            broadcast_msg_to_chat(msg, group_name=self.room_group_name, user='admin')
            return True
        return False

class ChatMessage(models.Model):
    thread = models.ForeignKey(Thread, null=True, blank=True, on_delete=models.SET_NULL, related_name="messages")
    user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='sender', on_delete=models.CASCADE)
    message = models.TextField()
    timestamp = models.DateTimeField(auto_now_add=True)

Django Chat Urls

from django.urls import include, path
from rest_framework import routers
from .views import ThreadViewSet, ChatMessageViewSet

router = routers.DefaultRouter()
router.register('thread', ThreadViewSet)
router.register('chatMessage', ChatMessageViewSet)

urlpatterns = [
    path('', include(router.urls)),
]

Expected Output I was expecting the "CONNECTED" message with the event information printed on the console according to the websocket_connect method in the consumers class. Similarly a "open" message with the event fired in my browser's console from the Angular 2+ chat.component.ts.

Actual Output

I immediately get this warning message on the browser console:

WebSocket connection to 'ws://localhost:4200/sockjs-node/344/ux0z32ma/websocket' failed: WebSocket is closed before the connection is established.

After about 2 minutes of waiting... Attached image shows what is automatically output on the console.

1 Answer 1

1

I may have missed something, because I cannot see the Django configuration, but as far as I understood, you wrote that you're running the frontend and backend server separately. You can see that the frontend is trying to establish the connection to localhost:4200. I belive that is the angular server which doesn't make sense, you should point your WebSocket to the Django app, so in my opinion you should modify the method below:

SERVER_URL = "localhost:8000"

getEndpoint() {
  let wsStart = "ws://"
  if (window.location.protocol == "https:") {
    wsStart = 'wss://'
  }
  this.endpoint = wsStart + SERVER_URL + window.location.pathname;
  return this.endpoint;
}

But if you're hosting both apps on Django and exposing that on port 4200 that doesn't apply, though I'm quite sure that's not your case.

Sign up to request clarification or add additional context in comments.

7 Comments

Thank you so much!! I had tried that earlier and tried it again. I got a different error: WebSocket connection to 'ws://localhost:8000/messages/1' failed: Error during WebSocket handshake: Unexpected response code: 403. I see! Yes! Earlier both were hosted on localhost:8000, now each is different. Front: :4200 and back-end at 127.0.0.1:8000.
@palebluedot Perfect, glad it did help! If the answer worked for you, I would appreciate if you accepted this as the correct answer. Cheers!
I got : WebSocket connection to 'ws://localhost:8000/messages/1' failed: Error during WebSocket handshake: Unexpected response code: 403. Do you know what might be causing this. At least now I know that the URL, should be the server url
AllowedHostsOriginValidator is using the ALLOWED_HOSTS from the Djnago settings. What's your current setup? My guess would be that you need to add the localhost:4200 host there.
Ahhhhhh!! I added it in! I think that worked. Now a routing error: [Failure instance: Traceback: <class 'ValueError'>: No route found for path 'messages/1'. I originally had "127.0.0.1" in the ALLOWED HOSTS so I had assumed that this is the same as "localhost" so I was under the assumption that I did not need to add anything. Prior to your response : ALLOWED_HOSTS = ['joole-api.herokuapp.com', '.herokuapp.com', '127.0.0.1']
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.