Django приложение, использующее Graphql и пакет Channels, бросает исключение внутри приложения: объект 'NoneType' не имеет атрибута 'replace'

У меня есть приложение Django, которое использует graphene для реализации GraphQL, у меня все настроено и работает, но теперь у меня есть ошибка в консоли, которая внезапно появилась, и хотя она ничего не нарушает, по крайней мере, насколько я могу судить, она продолжает появляться в консоли, и я хотел бы ее исправить.

Я совсем новичок в Django, поэтому не могу понять, откуда это происходит. Похоже, что это происходит из пакета channels.

Это ошибка в полном объеме, которая возникает сразу после запуска сервера и затем снова после выполнения каждого запроса.

Django version 3.2.3, using settings 'shuddhi.settings'
Starting ASGI/Channels version 3.0.3 development server at http://0.0.0.0:8000/
Quit the server with CONTROL-C.
WebSocket HANDSHAKING /graphql/ [172.28.0.1:60078]

Exception inside application: 'NoneType' object has no attribute 'replace'
Traceback (most recent call last):
  File "/usr/local/lib/python3.8/site-packages/channels/staticfiles.py", line 44, in __call__
    return await self.application(scope, receive, send)
  File "/usr/local/lib/python3.8/site-packages/channels/routing.py", line 71, in __call__
    return await application(scope, receive, send)
  File "/usr/local/lib/python3.8/site-packages/channels/security/websocket.py", line 35, in __call__
    if self.valid_origin(parsed_origin):
  File "/usr/local/lib/python3.8/site-packages/channels/security/websocket.py", line 54, in valid_origin
    return self.validate_origin(parsed_origin)
  File "/usr/local/lib/python3.8/site-packages/channels/security/websocket.py", line 73, in validate_origin
    return any(
  File "/usr/local/lib/python3.8/site-packages/channels/security/websocket.py", line 74, in <genexpr>
    pattern == "*" or self.match_allowed_origin(parsed_origin, pattern)
  File "/usr/local/lib/python3.8/site-packages/channels/security/websocket.py", line 98, in match_allowed_origin
    parsed_pattern = urlparse(pattern.lower(), scheme=None)
  File "/usr/local/lib/python3.8/urllib/parse.py", line 376, in urlparse
    splitresult = urlsplit(url, scheme, allow_fragments)
  File "/usr/local/lib/python3.8/urllib/parse.py", line 433, in urlsplit
    scheme = _remove_unsafe_bytes_from_url(scheme)
  File "/usr/local/lib/python3.8/urllib/parse.py", line 422, in _remove_unsafe_bytes_from_url
    url = url.replace(b, "")
AttributeError: 'NoneType' object has no attribute 'replace'

Это мой файл Settings.py:-

urls.py в папке main_project:-

from django.contrib import admin
from django.urls import include, path
from django.views.decorators.csrf import csrf_exempt
from graphene_django.views import GraphQLView
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('', include('app_name.urls')),
    path('admin/', admin.site.urls),
    path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True))),
]

if settings.DEBUG:
    urlpatterns += (
        static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
        )

urls.py в приложении:-

from . import views
from django.urls import path
from .views import *

urlpatterns = [
    path('', views.index, name='index'),
]

Файл Router.py, в котором я указал вещи, необходимые для подписки:-

from base64 import decode
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
from django.core.asgi import get_asgi_application
from django.urls import path
from .schema import MyGraphqlWsConsumer
from django.contrib.auth.models import AnonymousUser
from django.contrib.auth import get_user_model
from channels.db import database_sync_to_async
from channels.middleware import BaseMiddleware
import jwt
from .settings import SECRET_KEY
# from user.models import Token


@database_sync_to_async
def get_user(token_key):
    try:
        decodedPayload = jwt.decode(
            token_key, key=SECRET_KEY, algorithms=['HS256'])
        user_id = decodedPayload.get('sub')
        User = get_user_model()
        user = User.objects.get(pk=user_id)
        return user
    except Exception as e:
        return AnonymousUser()

# This is to enable authentication via websockets
# Source - https://stackoverflow.com/a/65437244/7981162


class TokenAuthMiddleware(BaseMiddleware):

    def __init__(self, inner):
        self.inner = inner

    async def __call__(self, scope, receive, send):
        query = dict((x.split("=")
                     for x in scope["query_string"].decode().split("&")))
        token_key = query.get("token")
        print('token from subscription request =>', token_key)
        scope["user"] = await get_user(token_key)
        print('user subscribing ', scope["user"])
        scope["session"] = scope["user"] if scope["user"] else None
        return await super().__call__(scope, receive, send)


application = ProtocolTypeRouter(
    {
        "http": get_asgi_application(),
        "websocket": AllowedHostsOriginValidator(TokenAuthMiddleware(
            URLRouter(
                [path("graphql/", MyGraphqlWsConsumer.as_asgi())]
            )
        )),
    }
)

Не уверен, что здесь нужно что-то еще, но это все, что я могу придумать. Было бы здорово узнать, как устранить неполадки и избавиться от этого исключения.

Обновление:-

Пока я продолжаю устранять неполадки, я обнаружил, что начал видеть это после того, как переместил переменную ALLOWED_HOSTS в файл env, и если я устанавливаю ALLOWED_HOSTS = ['*'] в файле settings.py, ошибка исчезает. И она появляется только тогда, когда происходит подписка из пользовательского интерфейса. Я определенно хочу, чтобы ALLOWED_HOSTS получала значение из переменной окружения, потому что оно будет разным для prod и dev и его нужно будет устанавливать из переменных окружения

Сейчас файл .env имеет следующее - DJANGO_ALLOWED_HOSTS=0.0.0.0,localhost

TlDR - Обновление пакета каналов до версии 3.0.4.

Детали: -

Я сузил проблему до того, что она вызвана тем, что ALLOWED_HOSTS имеет что-нибудь кроме ['*']. Таким образом, если у меня есть определенный список разрешенных доменов, например ['localhost', '0.0.0.0'], это приводит к исключению и фактически препятствует работе подписки.

Виной тому является использование 'AllowedHostsOriginValidator' пакета channels, который почему-то ломается при использовании Python 3.8. Проблема документирована здесь

В версию 3.0.4 пакета каналов добавлено исправление. Просто обновите пакет, и он будет работать нормально.

Вернуться на верх