I've got a Django app that uses graphene to implement GraphQL and I've got everything setup and working but I now have an error in the console which has popped up suddenly and although it doesn't break anything, at least from as far as what I can tell, it does keep showing up in the console and I'd like to fix it.
I'm quite new to Django so I'm not able to figure out where this is coming from. It looks like it's coming from the channels package.
This is the error in its entirety that happens immediately after the server runs and then again after every request is made.
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'
This is my Settings.py file:-
"""
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"
urls.py in the main_project folder:-
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 in the app:-
from . import views
from django.urls import path
from .views import *
urlpatterns = [
path('', views.index, name='index'),
]
Router.py file where I've specified stuff needed for subscriptions:-
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())]
)
)),
}
)
Not sure what else is necessary here, but this is all I can think of. It would be great to know how to troubleshoot and get rid of this exception.
Update:-
As I continue to troubleshoot this, I am discovering that I'm starting to see this after I moved the ALLOWED_HOSTS variable to the env file and if I set ALLOWED_HOSTS = ['*'] in the settings.py file, the error goes away. And it shows up only when the subscription from the UI happens. I definitely want to have the ALLOWED_HOSTS getting the value from the environment variable because it's going to be different for prod and dev and it will need to be set from the env variables.
Right now the .env file has this - DJANGO_ALLOWED_HOSTS=localhost,0.0.0.0
and it results in the ALLOWED_HOSTS being rendered as ['localhost', '0.0.0.0']