Hi,
I have an issue in django App;
Web Socket fails over https connection (please see attached screenshots of the console errors), while it works perfectly locally, with http;
I would appreciate your help, and also, advices on how to test web socket connection programmatically, so that I will not have regressions like this, in the near future
Thank you!
I have a similar issue; Locally with http, ws works perfectly;
In https, I receive connection interrupted
routing.py
# chat/routing.py
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r"ws/chat/(?P<room_name>\w+)/$", consumers.ChatConsumer.as_asgi()),
]
The chat is managed via:
urlpatterns = [
path("me", views.user_chats, name="user_chats"),
path("<int:room_id>", views.room, name="room"),
path("<int:room_id>/upload", views.upload_file, name="upload_file"),
]
The chat are server via https://taxcoder.cz/chat/2
The ngnix configuration is:
server {
listen 80;
server_name app.taxcoder.cz www.app.taxcoder.cz;
# Redirect all HTTP requests to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name app.taxcoder.cz www.app.taxcoder.cz;
# SSL certificate files
ssl_certificate /etc/letsencrypt/live/app.taxcoder.cz/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.taxcoder.cz/privkey.pem;
# WebSocket configuration
location /ws/chat/ {
proxy_pass https://web:8000/chat/; # Ensure your backend is running on HTTPS if using https
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;
# Optional WebSocket timeout settings
proxy_read_timeout 86400;
proxy_send_timeout 86400;
proxy_connect_timeout 86400;
proxy_buffering off;
}
# Other location blocks
location / {
proxy_pass http://web:8000; # Internal communication over HTTPS
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;
}
# Static files configuration
location /static/ {
alias /app/static/; # Adjust the alias to your static file location
}
}
Locally, it works, while when being in https, I receive the error to being closed
Script is:
<script>
const roomId = JSON.parse(document.getElementById('room-id').textContent);
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const chatSocket = new WebSocket(protocol + '//' + window.location.host + '/ws/chat/' + roomId + '/');
function scrollToLatestMessage() {
const chatLog = document.getElementById('chat-log');
chatLog.scrollTop = chatLog.scrollHeight;
}
document.addEventListener('DOMContentLoaded', function () {
scrollToLatestMessage();
updateInvoiceButtonVisibility(); // Check invoice button visibility on load
});
chatSocket.onmessage = function (e) {
const data = JSON.parse(e.data);
const senderName = `${data.first_name} ${data.last_name}`;
let messageElement;
if (data.message != null) {
messageElement = `<div><strong>${senderName}:</strong> ${data.message}</div>`;
} else {
let filename = data.file.split('/').pop();
let shortenedFilename;
if (filename.length > 15) {
shortenedFilename = `${filename.substring(0, 5)}...${filename.substring(filename.length - 8)}`;
} else {
shortenedFilename = filename;
}
messageElement = `<div><strong>${senderName}:</strong> <a href="${data.file}">File: ${shortenedFilename}</a></div>`;
}
const chatLog = document.getElementById('chat-log');
chatLog.innerHTML += messageElement;
scrollToLatestMessage();
updateInvoiceButtonVisibility();
};
chatSocket.onopen = function () {
console.log('WebSocket connection established');
};
chatSocket.onerror = function (error) {
console.error('WebSocket error:', error);
x };
chatSocket.onclose = function (event) {
console.log('WebSocket connection closed:', event.code, event.reason);
};
function sendMessage() {
const messageInputDom = document.getElementById('chat-message-input');
const message = messageInputDom.value;
if (message.trim() !== '') {
chatSocket.send(JSON.stringify({ message: message }));
messageInputDom.value = '';
}
}
document.getElementById('chat-message-submit').addEventListener('click', sendMessage);
document.getElementById('chat-message-input').addEventListener('keydown', function (event) {
if (event.key === 'Enter') {
sendMessage();
}
});
// Function to handle chat selection and button logic
function selectChat(clientId) {
localStorage.setItem('selectedClientId', clientId);
const chatLog = document.getElementById('chat-log');
chatLog.innerHTML = '';
const invoiceButton = document.getElementById('invoice-button');
invoiceButton.style.display = 'none';
invoiceButton.onclick = function() {
window.location.href = "{% url 'invoices' 0 %}".replace('0', clientId);
};
window.location.href = `/chat/${clientId}`;
}
function updateInvoiceButtonVisibility() {
const invoiceButton = document.getElementById('invoice-button');
const clientId = localStorage.getItem('selectedClientId');
const chatLog = document.getElementById('chat-log');
if (chatLog.innerHTML.trim() !== '' && clientId) {
invoiceButton.style.display = 'block';
invoiceButton.onclick = function() {
window.location.href = "{% url 'invoices' 0 %}".replace('0', clientId);
};
} else {
invoiceButton.style.display = 'none';
}
}
document.addEventListener('DOMContentLoaded', function () {
scrollToLatestMessage();
updateInvoiceButtonVisibility();
});
// Function to handle file upload
document.getElementById('chat-file-upload').addEventListener('click', async () => {
const input = document.getElementById('chat-file-select');
const files = input.files;
if (files.length === 0) {
alert('No files selected');
return;
}
const formData = new FormData();
for (const file of files) {
formData.append('files', file);
}
try {
const response = await fetch(`/chat/${roomId}/upload`, {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': "{{ csrf_token }}" // Ensure CSRF token is included for Django
}
});
const result = await response.json();
if (result.success) {
alert(result.success);
} else {
alert(result.error);
}
} catch (error) {
console.error('Error:', error);
}
input.value = ''; // Clear file input
});
</script>
Docker compose setup and dependency is:
services:
redis:
image: redis:7
container_name: redis_container
ports:
- "6379:6379"
restart: always
networks:
- taxcoder_network
web:
build: .
command: >
sh -c "python manage.py migrate &&
python manage.py collectstatic --noinput &&
python manage.py compilemessages &&
gunicorn --config gunicorn_config.py taxzen.wsgi:application"
volumes:
- .:/app
expose:
- "8000"
depends_on:
- redis
networks:
- taxcoder_network
restart: always
environment:
- DEBUG=False
- DJANGO_SETTINGS_MODULE=taxzen.settings
env_file:
- ./taxzen/.env
nginx:
image: nginx:latest
ports:
- "80:80"
- "443:443"
volumes:
- /etc/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- /etc/nginx/conf.d/taxzen.conf:/etc/nginx/conf.d/taxzen.conf:ro
- /var/www/certbot:/var/www/certbot
- .:/app
- /etc/letsencrypt:/etc/letsencrypt:ro
depends_on:
- web
networks:
- taxcoder_network
restart: always
networks:
taxcoder_network:
driver: bridge
volumes:
postgres_data:
Taxzen is setup at:
django_asgi_app = get_asgi_application()
application = ProtocolTypeRouter(
{
"http": django_asgi_app,
"websocket": AllowedHostsOriginValidator(
AuthMiddlewareStack(URLRouter(websocket_urlpatterns))
),
}
)
Here chat_consumer.py
class ChatConsumer(WebsocketConsumer):
'''
This method is called when a WebSocket connection is established.
It adds the user to the room group and loads the chat history.
'''
def connect(self):
self.room_name = self.scope["url_route"]["kwargs"]["room_name"]
self.room_group_name = f"chat_{self.room_name}"
# Join room group
async_to_sync(self.channel_layer.group_add)(
self.room_group_name, self.channel_name
)
# Accept the WebSocket connection
self.accept()
# Load and send chat history to the client
self.load_and_send_chat_history(self.room_name)