【发布时间】: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