【问题标题】:Django app using Graphql and Channels package throws Exception inside application: 'NoneType' object has no attribute 'replace'使用 Graphql 和 Channels 包的 Django 应用程序在应用程序内引发异常:“NoneType”对象没有属性“替换”
【发布时间】:2021-10-23 04:01:25
【问题描述】:

我有一个使用石墨烯来实现 GraphQL 的 Django 应用程序,我已经完成了所有设置和工作,但是我现在在控制台中遇到了一个错误,它突然弹出,尽管它至少没有破坏任何东西据我所知,它确实一直显示在控制台中,我想修复它。

我对 Django 很陌生,所以我无法弄清楚这是从哪里来的。它看起来像是来自频道包。

这是在服务器运行后立即发生的整个错误,然后在每次发出请求后再次发生。

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 文件:-

"""
Django settings for main_project project.

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

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

For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/
"""
from pathlib import Path
import os
import dj_database_url
from environ import Env              

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# BASE_DIR = Path(__file__).resolve().parent.parent

# This is to import the environment variables in the .env file
env = Env()                      

env.read_env(os.path.join(BASE_DIR, '.env'))  # This reads the environment variables from the .env file

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

# # # # # # # # # 
# Loading all environemtn Variables
# # # # # # # # 

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env('DJANGO_SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env.bool('DJANGO_DEBUG', default=False)
# Authorized origins
ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS')
# Whether or not requests from other origins are allowed
CORS_ORIGIN_ALLOW_ALL = env.bool('DJANGO_CORS_ORIGIN_ALLOW_ALL')
# Twilio Sendgrid API key
SENDGRID_API_KEY = env('SENDGRID_API_KEY')
# setting default email for sending email through sendgrid
DEFAULT_FROM_EMAIL = env('FROM_EMAIL_ID')

SENDGRID_SANDBOX_MODE_IN_DEBUG= env.bool('SENDGRID_SANDBOX_MODE_IN_DEBUG')
# Application definition


# Sendgrid Mail Settings
EMAIL_HOST = 'smtp.sendgrid.net'
EMAIL_HOST_USER = 'apikey' # this is exactly the value 'apikey'
EMAIL_HOST_PASSWORD = SENDGRID_API_KEY
EMAIL_PORT = 587
EMAIL_USE_TLS = True

INSTALLED_APPS = [
    'corsheaders',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app_name',
    'graphene_django',
    'graphql_jwt.refresh_token.apps.RefreshTokenConfig',
    'graphql_auth',
    'rest_framework',
    'django_filters',
    'channels'
]

GRAPHENE = {
    'SCHEMA': 'main_project.schema.schema',
    'MIDDLEWARE': [
        'graphql_jwt.middleware.JSONWebTokenMiddleware',
    ],
}

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

AUTHENTICATION_BACKENDS = [
    'graphql_auth.backends.GraphQLAuthBackend',
    'django.contrib.auth.backends.ModelBackend',
]

GRAPHQL_AUTH = {
    "ALLOW_LOGIN_NOT_VERIFIED": False
}

GRAPHQL_JWT = {
    "JWT_ALLOW_ANY_CLASSES": [
        "graphql_auth.mutations.Register",
        "graphql_auth.mutations.VerifyAccount",
        "graphql_auth.mutations.ResendActivationEmail",
        "graphql_auth.mutations.SendPasswordResetEmail",
        "graphql_auth.mutations.PasswordReset",
        "graphql_auth.mutations.ObtainJSONWebToken",
        "graphql_auth.mutations.VerifyToken",
        "graphql_auth.mutations.RefreshToken",
        "graphql_auth.mutations.RevokeToken",
    ],
    'JWT_PAYLOAD_HANDLER': 'common.utils.jwt_payload',
    "JWT_VERIFY_EXPIRATION": True,
    "JWT_LONG_RUNNING_REFRESH_TOKEN": True
}

EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'

ROOT_URLCONF = 'main_project.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates'), ],
        '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',
            ],
        },
    },
]

WSGI_APPLICATION = 'main_project.wsgi.application'

ASGI_APPLICATION = 'main_project.router.application'


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

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

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'main_projectdb',
        'USER': 'main_projectadmin',
        'PASSWORD': 'password',
        'HOST': 'db',
        'PORT': '5432',
    }
}

DATABASE_URL = os.environ.get('DATABASE_URL')
db_from_env = dj_database_url.config(default=DATABASE_URL, conn_max_age=500, ssl_require=True)
DATABASES['default'].update(db_from_env)

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


# Password validation
# https://docs.djangoproject.com/en/3.2/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.2/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/



STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')

STATIC_URL = '/static/'

STATICFILES_DIRS = (os.path.join(BASE_DIR, "static"),)

STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
# STATICFILES_STORAGE =  'django.contrib.staticfiles.storage.StaticFilesStorage' 

# Media files
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, "media")

# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

# This is here because we are using a custom User model
# https://docs.djangoproject.com/en/2.2/topics/auth/customizing/#substituting-a-custom-user-model
AUTH_USER_MODEL = "app_name.User"

main_project 文件夹中的 urls.py:-

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 文件后开始看到这个问题,如果我在 settings.py 文件中设置了 ALLOWED_HOSTS = ['*'],错误消失。它仅在 UI 订阅发生时显示。我绝对想让 ALLOWED_HOSTS 从环境变量中获取值,因为它对于 prod 和 dev 会有所不同,并且需要从 env 变量中设置。

现在 .env 文件有这个 - DJANGO_ALLOWED_HOSTS=localhost,0.0.0.0 并导致 ALLOWED_HOSTS 被呈现为 ['localhost', '0.0.0.0']

【问题讨论】:

    标签: python python-3.x django


    【解决方案1】:

    TlDR - 将频道包升级到 3.0.4。

    详情:-

    我已将问题范围缩小到它是由 ALLOWED_HOSTS 具有 ['*'] 以外的任何内容引起的。因此,如果我有一个特定的允许域列表,例如 ['localhost', '0.0.0.0'],它会引发异常并且实际上会阻止订阅工作。

    罪魁祸首是使用 channels 包的“AllowedHostsOriginValidator”,在使用 Python 3.8 时会以某种方式中断。该问题记录在here

    通道包的 3.0.4 版本已添加修复程序。只需升级它应该可以正常工作的软件包。

    【讨论】:

      猜你喜欢
      • 2021-08-05
      • 2020-09-13
      • 2021-06-13
      • 1970-01-01
      • 2011-12-27
      • 2018-10-17
      • 1970-01-01
      • 1970-01-01
      • 2016-12-22
      相关资源
      最近更新 更多