Problem with Deployment - Websocket is already in CLOSING or CLOSED state

I’m currently working on my first Django project, a counter app featuring two buttons on the index.html page for incrementing and decrementing a value. This project is a learning experience for me, and I’ve been relying on official documentation and ChatGPT as my primary resources. I apologize in advance for any basic questions I might have.

The app utilizes Django Channels and WebSockets to enable real-time updates of the counter value across multiple users. I’ve successfully tested the functionality locally and proceeded to deploy it using Docker. The deployment involved creating a Dockerfile, a docker-compose.yaml file, and then building and running the Docker image locally without issues.

Next, I transferred the Docker image to my IONOS VPS, placing the Dockerfile and docker-compose.yaml in a directory within the home folder. After navigating to this directory, I ran docker-compose up to start the application. Additionally, I configured Nginx by adding a my_apps.conf file to /etc/nginx/conf.d, which is included in the main nginx.conf.

However, I’ve encountered a problem: upon accessing the app via its URL, the page loads, but clicking the buttons doesn’t update the counter’s value. The terminal displays an error stating, “Websocket is already in CLOSING or CLOSED state.”

I’m unsure how to diagnose or fix this issue due to my limited experience with these technologies and deployment practices. Any guidance or suggestions would be greatly appreciated.

This is my NGINX conf:

upstream counter {
    server IP_CONTAINER:8015;  
}

# Server-Config counter
server {
    listen 443 ssl http2;
    server_name sub.domain.de;

    ssl_certificate path/to/fullchain1.pem;
    ssl_certificate_key path/to/privkey1.pem;

    location /ws/ {
        proxy_pass http://counter;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
} 

This is my Dockerfile:

FROM python:3.12.1

WORKDIR /app

COPY requirements.txt /app/

RUN pip install --no-cache-dir -r requirements.txt

COPY . /app/

CMD ["daphne", "-b", "0.0.0.0", "-p", "8015", "countnow.asgi:application"]

This is my docker-compose.yaml:

version: '3.8'

services:
  web:
    build: .
    image: isaiur/countnow:deploy_ws
    command: daphne -b 0.0.0.0 -p 8015 countnow.asgi:application
    environment:
      - DJANGO_SETTINGS_MODULE=countnow.settings
    volumes:
      - .:/app
    ports:
      - "8015:8015"
    depends_on:
      - redis

  redis:
    image: "redis:7"
    ports:
      - "6379:6379"

and my CHANNEL_LAYERS in settings.py:

CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('redis', 6379)],
        },
    },
}

and I think it could be helpful my project/routing.py:

from channels.routing import ProtocolTypeRouter, URLRouter
import counter.routing

application = ProtocolTypeRouter({
    'websocket': URLRouter(counter.routing.websocket_urlpatterns),
})

and my app/routing.py:

from django.urls import re_path
from . import consumers

websocket_urlpatterns = [
    re_path(r'ws/counter/$', consumers.CounterConsumer.as_asgi()),
]

and my asgi.py:

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

I am very grateful for any help. TIA

What is your code in your JavaScript that is opening and using the websocket?

Note: I wouldn’t try using http2 with this until you get a standard http version working.

Thanks for asking! This is on my counter.html:

<script>
    const socket = new WebSocket('wss://' + window.location.host + '/ws/counter/');

    socket.onmessage = function(e) {
        const data = JSON.parse(e.data);
        const newValue = data.value;

        document.getElementById("counterValue").innerText = "Counter: " + newValue;
    };

    document.getElementById("incrementButton").addEventListener("click", function() {
        socket.send(JSON.stringify({
            'action': 'increment'
        }));
    });

    document.getElementById("decrementButton").addEventListener("click", function() {
        socket.send(JSON.stringify({
            'action': 'decrement'
        }));
    });
</script>

and this the consumers.py:

class CounterConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        await self.channel_layer.group_add("counter", self.channel_name)
        await self.accept()

    async def disconnect(self, close_code):
        await self.channel_layer.group_discard("counter", self.channel_name)

    async def receive(self, text_data):
        text_data_json = json.loads(text_data)
        action = text_data_json['action']
        
        # Handle the counter logic here
        counter, _ = await database_sync_to_async(Counter.objects.get_or_create)(id=1)
        if action == 'increment':
            counter.value = F('value') + 1
        elif action == 'decrement':
            if counter.value > 0:
                counter.value = F('value') - 1
        await database_sync_to_async(counter.save)()
        
        # Retrieve the updated value to send it
        await database_sync_to_async(counter.refresh_from_db)()

        # Broadcast the new counter value to everyone in the "counter" group
        await self.channel_layer.group_send(
            "counter",
            {
                'type': 'counter_update',
                'value': counter.value,
            }
        )

    async def counter_update(self, event):
        value = event['value']
        await self.send(text_data=json.dumps({
            'value': value
        }))

I am not sure about two things if they are relevant:

I am using bootstrap and I don’t know if there is a conflict when using django 5.0 and django channels 4.0.
After I installed everything I ran in the terminal: pip freeze > requirements.txt and after docker-compose up --build I got an error that any Twisted and twistedio could not get installed. When I google Twisted I only find that it has anything to do with http2.

On Plesk (IONOS Server Management) I installed a Let’s encrypt for the url.

First, I think one issue is:

Since your routing is expecting to get ws as the beginning of the url submitted to it, this should be:

    location /ws/ {
        proxy_pass http://counter/ws/;

(Or possibly defined in the server directive of the upstream specification. I’m not sure, I’ve never user the upstream directive - I always specify the destination directly in the proxy_pass directive.)

Also, in your consumer, you have a potential problem at:

Two items here you may want to address / account for:

  • If you receive a websocket frame that isn’t valid JSON
  • If you receive a websocket frame that doesn’t contain the key action

Either case is going to throw an error internally, breaking your consumer.

Additionally, whenever you’re trying to diagnose issues like this, you’ll want to check both the nginx and the Daphne logs.

Regarding your additional items:

Bootstrap is a browser-only issue - it has nothing to do here.

However, Twisted is critical - Daphne is a Twisted application. If Twisted isn’t available, Daphne won’t run.

Finally, I also use Let’s Encrypt for my ssl certs. They work great!

1 Like

Thank you! I will look into this and hope to come back with any problems.

I can’t solve the issue with Twisted …

I am getting this error:

26.18 Building wheels for collected packages: autobahn, twisted-iocpsupport
26.20   Building wheel for autobahn (setup.py): started
27.25   Building wheel for autobahn (setup.py): finished with status 'done'
27.25   Created wheel for autobahn: filename=autobahn-23.6.2-py2.py3-none-any.whl size=666845 sha256=fb6c45f5e1451a1bb0e185a0904c95f68c205e8d572b0fa8532b700a3be43457
27.25   Stored in directory: /tmp/pip-ephem-wheel-cache-gjjfpu84/wheels/91/de/ea/aa1c040ea3fa2c92276d081c13a415aca31786456f15abcc86
27.26   Building wheel for twisted-iocpsupport (pyproject.toml): started
27.94   Building wheel for twisted-iocpsupport (pyproject.toml): finished with status 'error'
27.95   error: subprocess-exited-with-error
27.95
27.95   × Building wheel for twisted-iocpsupport (pyproject.toml) did not run successfully.
27.95   │ exit code: 1
27.95   ╰─> [13 lines of output]
27.95       running bdist_wheel
27.95       running build
27.95       running build_ext
27.95       building 'twisted_iocpsupport.iocpsupport' extension
27.95       creating build
27.95       creating build/temp.linux-x86_64-cpython-310
27.95       creating build/temp.linux-x86_64-cpython-310/twisted_iocpsupport
27.95       gcc -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -Itwisted_iocpsupport -I/usr/local/include/python3.10 -c twisted_iocpsupport/iocpsupport.c -o build/temp.linux-x86_64-cpython-310/twisted_iocpsupport/iocpsupport.o
27.95       twisted_iocpsupport/iocpsupport.c:1215:10: fatal error: io.h: No such file or directory
27.95        1215 | #include "io.h"
27.95             |          ^~~~~~
27.95       compilation terminated.
27.95       error: command '/usr/bin/gcc' failed with exit code 1
27.95       [end of output]
27.95
27.95   note: This error originates from a subprocess, and is likely not a problem with pip.
27.95   ERROR: Failed building wheel for twisted-iocpsupport
27.95 Successfully built autobahn
27.95 Failed to build twisted-iocpsupport
27.95 ERROR: Could not build wheels for twisted-iocpsupport, which is required to install pyproject.toml-based projects

after googling I added those lines to my dockerfile, so the updated version is this:

FROM python:3.12.1

RUN apt-get update && apt-get install -y \
    build-essential \
    libssl-dev \
    libffi-dev \
    python3-dev

WORKDIR /app

COPY requirements.txt /app/

RUN pip install --no-cache-dir -r requirements.txt

COPY . /app/

CMD ["daphne", "-b", "0.0.0.0", "-p", "8015", "countnow.asgi:application"]

and I try to use FROM python:3.10 but it didn’t work either.

What do you have that is trying to load twisted-iocpsupport? What I’m reading is that this is only used in a Windows environment, and that you wouldn’t need this in a Linux docker container.

So sorry, but I don’t get it.
My machine is running on Windows 11. After setting up the project in a venv I ran pip freeze > requirements.txt.

I need daphne for using websockets and django channels and Twisted is necessary for daphne? So I tried to fix this error when running docker-compose up --build after creating a fresh requirements.txt. When I manually delete Twisted and twisted-iocpsupport the docker-compose up --build -d runs without an error.

I wanted to test if it works with the new nginx configuration but I ran into a new error which I have no idea where this is coming from …

As answered above, yes. If you do a pip install daphne, it will install Twisted if its not already installed. You don’t need to specify it in your requirements.txt file. What you specifically don’t want in your requirements.txt file is the reference to twisted-iocpsupport.

1 Like

So, over the weekend I tried several things. Unfortunately I still didn’t fix the Websocket Connection problem.
Also I searched a little bit in this forum and tried different stuff I found …

But the twistedio-problem is solved. I removed it from the requirements.txt and added Twisted and that works perfectly.

May I ask you for help on my new snippets:

This is my routing.py in the app-folder:

from django.urls import re_path
from . import consumers

websocket_urlpatterns = [
    re_path(r'^ws/counter/(?P<name>[^/]+)/?$', consumers.CounterConsumer.as_asgi()),
]

this is inside the .html:

<script>
    var ws_scheme = window.location.protocol == "https:" ? "wss" : "ws";
    var name = "{{ counter.name|default:counter.id }}"; // Benutzt counter.id als Fallback, wenn counter.name leer ist
    var ws_path = ws_scheme + '://' + window.location.host + '/ws/counter/' + name;
    console.log(ws_path);
    var websocket = new WebSocket(ws_path);

    websocket.onmessage = function(e) {
        const data = JSON.parse(e.data);
        const newValue = data.value;

        document.getElementById("counterValue").innerText = "Counter: " + newValue;
    };

    document.getElementById("incrementButton").addEventListener("click", function() {
        websocket.send(JSON.stringify({
            'action': 'increment'
        }));
    });

    document.getElementById("decrementButton").addEventListener("click", function() {
        websocket.send(JSON.stringify({
            'action': 'decrement'
        }));
    });
</script>

My consumers.py is still the same.

and my nginx configuration:

#countnow
upstream countnow {
    server IP_docker_container:8015;  
}

# Server-Konfiguration countnow
server {
    listen 443 ssl;
    server_name countnow.prosicherheit.de;

    ssl_certificate /path/to/fullchain1.pem;
    ssl_certificate_key /path/to/privkey1.pem;

    location /ws/ {
        proxy_pass http://countnow/ws/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Origin $http_origin;
        proxy_read_timeout 86400;
    }
}

In the browser I am still getting the same error and when I run ‘docker-compose up’ in the terminal in vs code and open localhost:8015/counter/SOMETHING I am getting this error:

[+] Running 3/3
 ✔ Network deploy_ws_default    Created                                                                       0.1s 
 ✔ Container deploy_ws-redis-1  Created                                                                       0.2s 
 ✔ Container deploy_ws-web-1    Created                                                                       0.2s 
Attaching to redis-1, web-1
redis-1  | 1:C 25 Mar 2024 18:23:23.302 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
redis-1  | 1:C 25 Mar 2024 18:23:23.302 # Redis version=7.0.12, bits=64, commit=00000000, modified=0, pid=1, just started
redis-1  | 1:C 25 Mar 2024 18:23:23.302 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
redis-1  | 1:M 25 Mar 2024 18:23:23.303 * monotonic clock: POSIX clock_gettime
redis-1  | 1:M 25 Mar 2024 18:23:23.304 * Running mode=standalone, port=6379.
redis-1  | 1:M 25 Mar 2024 18:23:23.304 # Server initialized
redis-1  | 1:M 25 Mar 2024 18:23:23.304 * Ready to accept connections
web-1    | 2024-03-25 18:23:24,437 INFO     Starting server at tcp:port=8015:interface=0.0.0.0
web-1    | 2024-03-25 18:23:24,437 INFO     HTTP/2 support enabled
web-1    | 2024-03-25 18:23:24,437 INFO     Configuring endpoint tcp:port=8015:interface=0.0.0.0
web-1    | 2024-03-25 18:23:24,438 INFO     Listening on TCP address 0.0.0.0:8015
web-1    | 2024-03-25 18:23:37,453 ERROR    Exception inside application: No route found for path 'ws/counter/test'.
web-1    | Traceback (most recent call last):
web-1    |   File "/usr/local/lib/python3.12/site-packages/channels/routing.py", line 62, in __call__
web-1    |     return await application(scope, receive, send)
web-1    |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
web-1    |   File "/usr/local/lib/python3.12/site-packages/channels/sessions.py", line 47, in __call__
web-1    |     return await self.inner(dict(scope, cookies=cookies), receive, send)
web-1    |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
web-1    |   File "/usr/local/lib/python3.12/site-packages/channels/sessions.py", line 263, in __call__
web-1    |     return await self.inner(wrapper.scope, receive, wrapper.send)
web-1    |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
web-1    |   File "/usr/local/lib/python3.12/site-packages/channels/auth.py", line 185, in __call__
web-1    |     return await super().__call__(scope, receive, send)
web-1    |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
web-1    |   File "/usr/local/lib/python3.12/site-packages/channels/middleware.py", line 24, in __call__
web-1    |     return await self.inner(scope, receive, send)
web-1    |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
web-1    |   File "/usr/local/lib/python3.12/site-packages/channels/routing.py", line 134, in __call__
web-1    |     raise ValueError("No route found for path %r." % path)
web-1    | ValueError: No route found for path 'ws/counter/test'.

If it helps, here is my views.py for dynamically generate the url:

from django.shortcuts import render, redirect
from .models import Counter

def counter_view(request, name=None):
    if name:
        counter, _ = Counter.objects.get_or_create(name=name)
    else:
        # Erstellt einen neuen Counter-Eintrag und verwendet dessen ID als Name
        counter = Counter.objects.create()
        return redirect('counter_view', name=counter.id)

    return render(request, 'counter/counter.html', {'counter': counter})

Thank you so much in advance.

What I am also wondering about are the versions in my requirements.txt (this is a cutout):

channels==4.0.0
channels-redis==4.1.0
daphne==4.0.0
Django==5.0
redis==5.0.1

I tried to downgrade but I got stuck in a circle of errors.

You should not be opening that port from your browser or your JavaScript. The reason you have this behind nginx is so that nginx will route this to Daphne.

Your JavaScript code in the browser should be connecting to your 443 port that nginx is listening on.

My bad. That is this way because I copied the error when tested it locally after “docker-compose up” in the terminal.

It is the same when using “docker-compose up” on the server … I also get ‘ws/counter/something’ couldn’t be found

What is the code that you are using to connect to the websocket?

It’s this:

class CounterConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        await self.channel_layer.group_add("counter", self.channel_name)
        await self.accept()

    async def disconnect(self, close_code):
        await self.channel_layer.group_discard("counter", self.channel_name)

    async def receive(self, text_data):
        text_data_json = json.loads(text_data)
        action = text_data_json['action']
        
        # Handle the counter logic here
        counter, _ = await database_sync_to_async(Counter.objects.get_or_create)(id=1)
        if action == 'increment':
            counter.value = F('value') + 1
        elif action == 'decrement':
            if counter.value > 0:
                counter.value = F('value') - 1
        await database_sync_to_async(counter.save)()
        
        # Retrieve the updated value to send it
        await database_sync_to_async(counter.refresh_from_db)()

        # Broadcast the new counter value to everyone in the "counter" group
        await self.channel_layer.group_send(
            "counter",
            {
                'type': 'counter_update',
                'value': counter.value,
            }
        )

    async def counter_update(self, event):
        value = event['value']
        await self.send(text_data=json.dumps({
            'value': value
        }))

or do you mean something else?

No, I’m asking about from the browser. How is the browser connecting to the websocket to connect with the consumer.

<script>
    var websocket; 

    function connectWebSocket() {
        var ws_scheme = window.location.protocol == "https:" ? "wss" : "ws";
        var name = "{{ counter.name|default:counter.id }}"; 
        var ws_path = ws_scheme + '://' + window.location.host + '/ws/counter/' + name;
        console.log("Connecting to " + ws_path);
        websocket = new WebSocket(ws_path);

        websocket.onopen = function() {
            console.log('WebSocket connection successfully established');
        };

        websocket.onmessage = function(e) {
            const data = JSON.parse(e.data);
            const newValue = data.value;
            document.getElementById("counterValue").innerText = "Counter: " + newValue;
        };

        websocket.onerror = function(error) {
            console.error('WebSocket error:', error);
        };

        websocket.onclose = function(e) {
            console.error('WebSocket connection closed:', e);
            console.log('Attempting to reconnect...');
            setTimeout(connectWebSocket, 1000); // Try to reconnect after 1 second
        };
    }

    function sendMessage(action) {
        if (websocket.readyState === WebSocket.OPEN) {
            websocket.send(JSON.stringify({ 'action': action }));
        } else {
            console.error('WebSocket is not open. Current state:', websocket.readyState);
        }
    }

    document.addEventListener('DOMContentLoaded', () => {
        connectWebSocket(); 

        document.getElementById("incrementButton").addEventListener("click", function() {
            sendMessage('increment');
        });

        document.getElementById("decrementButton").addEventListener("click", function() {
            sendMessage('decrement');
        });
    });
</script>

I changed some things and now I am getting this error:

You’re not showing the errors being generated on the server side from this - it’s important to provide the complete picture when trying to identify a cause of the error.

But what I do see is that you have a data-type error.

In your browser’s JavaScript, you have:

Which is going to send a string.

However, in your consumer, you have:

Which is expecting a JSON object to be received.

1 Like

Thanks! I’ll have a look at that. I thought I had previously converted the string to json in my JavaScript