Custom Django Channels middleware not finishing processing before Websocket connects - django

I have an existing WSGI application which I'm adding Django Channels to to give websocket functionality. I created a consumer using WebsocketConsumer, added the custom middleware into the routing file, and implemented a basic version of pulling the token from the incoming connection request. I can successfully print the token that's in the database, so I know the correct information is passing.
I can connect to the socket, but it always comes back as being an anonymous user within the scope. It seems that the get_user_from_token function is not getting a chance to execute before the connect function executes, because all of the prints within the __call__ function of the TokenAuthMiddleware class are printed and none of the prints from the get_user_from_Token are printing. I tried switching the consumer to an async consumer, but that opened up a whole other set of problems that I couldn't figure out. I tried putting async in front of the __call__ and await in front of the function call, but that didn't work either. The current error I'm getting is:
Exception inside application: 'coroutine' object has no attribute '_wrapped'
File "C:\Users\PC\Envs\p3\lib\site-packages\channels\sessions.py", line 183, in __call__
return await self.inner(receive, self.send)
File "C:\Users\PC\Envs\p3\lib\site-packages\channels\middleware.py", line 40, in coroutine_
call
await self.resolve_scope(scope)
File "C:\Users\PC\Envs\p3\lib\site-packages\channels\auth.py", line 166, in resolve_scope
scope["user"]._wrapped = await get_user(scope)
'coroutine' object has no attribute '_wrapped'
How do I get my middleware to finish what it's doing before connect tries to test the user?
my_app/routing.py
from channels.routing import ProtocolTypeRouter, URLRouter
import api.channels.routing
from my_app.ws_token_auth import TokenAuthMiddlewareStack
application = ProtocolTypeRouter({
# (http->django views is added by default)
'websocket': TokenAuthMiddlewareStack(
URLRouter(
api.channels.routing.websocket_urlpatterns
)
),
})
api/channels/consumers.py
import json
from asgiref.sync import async_to_sync
from channels.db import database_sync_to_async
from channels.generic.websocket import WebsocketConsumer
class HeaderConsumer(WebsocketConsumer):
def connect(self):
if self.scope["user"].is_anonymous:
# Reject the connection
print('rejected')
self.close()
else:
self.accept()
self.user = self.scope['user']
self.message_threads = set()
def disconnect(self, code):
"""
Called when the WebSocket closes for any reason.
"""
# Leave all the rooms we are still in
for thread_id in list(self.message_threads):
try:
self.leave_thread(thread_id)
except ClientError:
pass
def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
self.send(text_data=json.dumps({
'message': message + message
}))
my_app/ws_token_auth.py
from channels.auth import AuthMiddlewareStack
from channels.db import database_sync_to_async
from rest_framework.authtoken.models import Token
from django.contrib.auth.models import AnonymousUser
from django.db import close_old_connections
#database_sync_to_async
def close_connections():
close_old_connections()
#database_sync_to_async
def get_user_from_token(t):
try:
print("trying token" + t)
token = Token.objects.get(token=t).prefetch_related('user')
return token.user
except Token.DoesNotExist:
print("failed")
return AnonymousUser()
class TokenAuthMiddleware:
"""
Token authorization middleware for Django Channels 2
"""
def __init__(self, inner):
self.inner = inner
def __call__(self, scope):
close_connections()
print("hi")
headers = dict(scope['headers'])
if b'cookie' in headers:
pieces = headers[b'cookie'].decode().split("; ")
key_values = {i.split('=', 1)[0]: i.split('=', 1)[1] for i in pieces}
print("x")
if 'token' in key_values:
try:
scope['token'] = key_values['token']
print("y")
user = get_user_from_token(key_values['token'])
print("z")
except Token.DoesNotExist:
print("no token")
user = AnonymousUser()
else:
print("no token?")
else:
print("no cookie")
return self.inner(dict(scope, user=user))
TokenAuthMiddlewareStack = lambda inner: TokenAuthMiddleware(AuthMiddlewareStack(inner))

Change you class HeaderConsumer(WebsocketConsumer): with
class HeaderConsumer(AsyncWebsocketConsumer):
And also check if your websocket_urlpatterns:
websocket_urlpatterns = [
re_path(r'your path', consumers.HeaderConsumer.as_asgi()),
]

Related

Best practices for authenticating Django Channels

Django 4.1.4
djoser 2.1.0
channels 4.0.0
I have followed the documented recommendation for creating custom middleware to authenticate a user when using channels and I am successfully getting the user
and checking that the user is authenticated though I am sending the user ID in the querystring when connecting to the websocket to do this. The user is not automatically available in the websocket scope.
I am unsure if there are any potential security risks as the documentation mentions that their recommendation is insecure, I do check that the user.is_authenticated. So I believe I have secured it.
I do believe that using the token created by djoser would be better though I am not sure how to send headers with the websocket request unless I include the token in the querystring instead of the user's ID.
I am keen to hear what the best practices are.
I am passing the user ID to the websocket via querystring as follows at the frontend:
websocket.value = new WebSocket(`ws://127.0.0.1:8000/ws/marketwatch/? ${authStore.userId}`)
middleware.py
from channels.db import database_sync_to_async
from django.contrib.auth.models import AnonymousUser
from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist
#database_sync_to_async
def get_user(user_id):
User = get_user_model()
try:
user = User.objects.get(id=user_id)
except ObjectDoesNotExist:
return AnonymousUser()
else:
if user.is_authenticated:
return user
else:
return AnonymousUser()
class QueryAuthMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
scope['user'] = await get_user(int(scope["query_string"].decode()))
return await self.app(scope, receive, send)
consumers.py
import os
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
from channels.security.websocket import AllowedHostsOriginValidator
from api.middleware import QueryAuthMiddleware
from .routing import ws_urlpatterns
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'api.settings')
application = ProtocolTypeRouter({
'http':get_asgi_application(),
'websocket': AllowedHostsOriginValidator(
QueryAuthMiddleware(
URLRouter(ws_urlpatterns)
)
)
})
After doing some extensive research I decided not to pass the id or the token via the querystring as this poses a risk due to this data being stored in the server logs.
IMO the best option with the least amount of risk was passing the token as a message to the websocket after the connection was established and then verifying the token; closing the websocket if invalid.
This meant not requiring the middleware previously implemented. In this particular project no other messages would be received from the client so I don't need to do any checking on the key of the message received. This could be changed for chat apps and other apps that will receive further messages from the client.
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
import json
from rest_framework.authtoken.models import Token
class MarketWatchConsumer(AsyncWebsocketConsumer):
#database_sync_to_async
def verify_token(self, token_dict):
try:
token = Token.objects.get(key=token_dict['token'])
except Token.DoesNotExist:
return False
else:
if token.user.is_active:
return True
else:
return False
async def connect(self):
await self.channel_layer.group_add('group', self.channel_name)
await self.accept()
async def receive(self, text_data=None, bytes_data=None):
valid_token = await self.verify_token(json.loads(text_data))
if not valid_token:
await self.close()
async def disconnect(self, code):
await self.channel_layer.group_discard('group', self.channel_name)

django channels fetch data from db and send over websocket

error:
File "/Users/soubhagyapradhan/Desktop/upwork/polyverse/polyverse_api/env/lib/python3.8/site-packages/django/db/models/sql/compiler.py", line 1173, in execute_sql
cursor = self.connection.cursor()
File "/Users/soubhagyapradhan/Desktop/upwork/polyverse/polyverse_api/env/lib/python3.8/site-packages/django/utils/asyncio.py", line 24, in inner
raise SynchronousOnlyOperation(message)
django.core.exceptions.SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async.
WebSocket DISCONNECT /ws/test/ [127.0.0.1:63954]
comsumer.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer
from random import randint
from api.models import Asset
class WSConsumer(AsyncWebsocketConsumer):
async def connect(self):
await self.accept()
res = list(Asset.objects.filter(id=56).values('token_id'))
await self.send(json.dumps(res))
here is the code i am using for websocket. here
i am fetching data from database but getting above error
please take a look what will be the error ?
I think you need database_sync_async like this:
class WSConsumer(AsyncWebsocketConsumer):
# rest of the code
res = await database_sync_to_async(self.get_asset(56))()
# rest of the code
def get_asset(self, id):
return list(Asset.objects.filter(id=id).values('token_id'))
I believe it is because Django ORM is synchronous.

Django Channel Custom Authentication Middleware __call__() missing 2 required positional arguments: 'receive' and 'send'

I am writing a custom authentication middleware for django channels
class TokenAuthMiddleware:
def __init__(self, inner):
# Store the ASGI application we were passed
self.inner = inner
def __call__(self, scope):
return TokenAuthMiddlewareInstance(scope, self)
class TokenAuthMiddlewareInstance:
def __init__(self, scope, middleware):
self.middleware = middleware
self.scope = dict(scope)
self.inner = self.middleware.inner
async def __call__(self, receive, send):
## my logic to get validate user and store the user in user data
...
...
...
self.scope['user'] = user_data
inner = self.inner(self.scope)
return await inner(receive, send)
but on trying to connect to web socket from front end I get the following error
TypeError: __call__() missing 2 required positional arguments: 'receive' and 'send'
For your reference: https://channels.readthedocs.io/en/stable/releases/3.0.0.html
change from in routing.py
websocket_urlpatterns = [
re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer),
]
to
Consumers now have an as_asgi() class method you need to call when setting up your routing:
websocket_urlpatterns = [
re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),
]
then
if you need custom authentication https://channels.readthedocs.io/en/stable/topics/authentication.html#custom-authentication
from channels.auth import AuthMiddlewareStack
from channels.db import database_sync_to_async
from django.contrib.auth import get_user_model
User = get_user_model()
#database_sync_to_async
def get_user(user_id):
try:
return User.objects.get(id=user_id)
except User.DoesNotExist:
return AnonymousUser()
class QueryAuthMiddleware:
"""
Custom middleware (insecure) that takes user IDs from the query string.
"""
def __init__(self, app):
# Store the ASGI application we were passed
self.app = app
async def __call__(self, scope, receive, send):
# Look up user from query string (you should also do things like
# checking if it is a valid user ID, or if scope["user"] is already
# populated).
scope['user'] = await get_user(int(scope["query_string"]))
return await self.app(scope, receive, send)
TokenAuthMiddlewareStack = lambda inner: QueryAuthMiddleware(AuthMiddlewareStack(inner))
use requirements.txt as following list, and also download package in this order
Django==3.0.8
djangorestframework==3.11.0
websocket-client==0.57.0
redis==3.5.3
asgiref==3.2.10
channels-redis==2.4.2
channels==3.0.1
As Yuva Raja has said, in Django Channels version 3 you need to set your path as:
websocket_urlpatterns = [
re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),
]
One more thing, in their Custom authentication official documentation they forgot to use scope instead of self.scope.
So be sure to use:
scope['user'] = await get_user(int(scope["query_string"]))
instead of their example:
scope['user'] = await get_user(int(self.scope["query_string"]))
This error happened to me when I installed channels==2.4.0
Updating channels to channels==3.0.3 (latest at the moment) fixed the issue!
I managed to make it work this way, with Django Channels 3:
from django.contrib.auth.models import AnonymousUser
from rest_framework.authtoken.models import Token
from channels.auth import AuthMiddlewareStack
from channels.db import database_sync_to_async
import urllib.parse
#database_sync_to_async
def get_user(token):
try:
token = Token.objects.get(key=token)
return token.user
except Token.DoesNotExist:
return AnonymousUser()
class TokenAuthMiddleware:
def __init__(self, inner):
self.inner = inner
def __call__(self, scope):
return TokenAuthMiddlewareInstance(scope, self)
class TokenAuthMiddlewareInstance:
"""
Yeah, this is black magic:
https://github.com/django/channels/issues/1399
"""
def __init__(self, scope, middleware):
self.middleware = middleware
self.scope = dict(scope)
self.inner = self.middleware.inner
async def __call__(self, receive, send):
decoded_qs = urllib.parse.parse_qs(self.scope["query_string"])
if b'token' in decoded_qs:
token = decoded_qs.get(b'token').pop().decode()
self.scope['user'] = await get_user(token)
return await self.inner(self.scope, receive, send)
TokenAuthMiddlewareStack = lambda inner: TokenAuthMiddleware(AuthMiddlewareStack(inner))
from urllib.parse import parse_qs
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.db import close_old_connections
from channels.auth import AuthMiddlewareStack
from rest_framework_simplejwt.tokens import AccessToken
from channels.db import database_sync_to_async
User = get_user_model()
#database_sync_to_async
def get_user(user_id):
try:
return User.objects.get(id=user_id)
except User.DoesNotExist:
return AnonymousUser()
class TokenAuthMiddleware:
def __init__(self, inner):
self.inner = inner
async def __call__(self, scope, receive, send):
close_old_connections()
query_string = parse_qs(scope['query_string'].decode())
token = query_string.get('token')
if not token:
scope['user'] = AnonymousUser()
return await self.inner(scope, receive, send)
access_token = AccessToken(token[0])
user = await get_user(access_token['id'])
if isinstance(user, AnonymousUser):
scope['user'] = AnonymousUser()
return await self.inner(scope, receive, send)
if not user.is_active:
scope['user'] = AnonymousUser()
return await self.inner(scope, receive, send)
scope['user'] = user
return await self.inner(scope, receive, send)
TokenAuthMiddlewareStack = lambda inner: TokenAuthMiddleware(AuthMiddlewareStack(inner))
from channels.db import database_sync_to_async
from rest_framework.authtoken.models import Token
from django.contrib.auth.models import AnonymousUser
from urllib.parse import parse_qs
from channels.auth import AuthMiddlewareStack
#database_sync_to_async
def get_user(scope):
try:
token_key=parse_qs(scope['query_string'].decode("utf8"))["token"][0]
token=Token.objects.get(key=token_key)
return token.user
except Token.DoesNotExist:
return AnonymousUser()
class TokenAuthMiddleware:
def __init__(self,inner):
self.inner=inner
def __call__(self,scope):
return TokenAuthMiddlewareInstance(scope,self)
class TokenAuthMiddlewareInstance:
def __init__(self,scope,middleware):
self.middleware = middleware
self.scope=dict(scope)
self.inner=self.middleware.inner
async def __call__(self,receive,send):
self.scope['user'] = await get_user(self.scope)
return await self.inner(self.scope,receive,send)
TokenAuthMiddlewareStack = lambda inner: TokenAuthMiddleware(AuthMiddlewareStack(inner))
I got an error 2 positional argument missing receive and send
Traceback (most recent call last):
File "D:\Project\IQ Tester\env\lib\site-packages\channels\staticfiles.py", line 44, in __call__
return await self.application(scope, receive, send)
File "D:\Project\IQ Tester\env\lib\site-packages\channels\routing.py", line 71, in __call__
return await application(scope, receive, send)
File "D:\Project\IQ Tester\env\lib\site-packages\asgiref\compatibility.py", line 34, in new_application
return await instance(receive, send)
File "D:\Project\IQ Tester\backend\account\token_auth_middleware.py", line 40, in __call__
inner=self.inner(self.scope)
TypeError: __call__() missing 2 required positional arguments: 'receive' and 'send'
so to solve this I write this
return await self.inner(self.scope,receive,send)
instead of this
self.scope['user'] = user_data
inner = self.inner(self.scope)
return await inner(receive, send)

Django jwt middleware for channels websocket authentication

I'm trying to set a Authentication middleware for django channels. I want this middleware to be active only for websocket requests.
Seems like that in this case i don't get a full middleware functionality. For example i can't get response = self.get_response(scope) working:
'TokenAuthMiddleware' object has no attribute 'get_response'
Everything is allright with this middleware now (it is activated only for websocket requests and not registered in settings.py), except that i need a means to modify a response status codes (block anonymous users and set the error code for ExpiredSignatureError). Any help appreciated. I use Django 2.0.6 and channels 2.1.1. jwt authentication by djangorestframework-jwt
middleware:
import jwt, re
import traceback
import logging
from channels.auth import AuthMiddlewareStack
from django.contrib.auth.models import AnonymousUser
from django.conf import LazySettings
from jwt import InvalidSignatureError, ExpiredSignatureError, DecodeError
from project.models import MyUser
settings = LazySettings()
logger = logging.getLogger(__name__)
class TokenAuthMiddleware:
"""
Token authorization middleware for Django Channels 2
"""
def __init__(self, inner):
self.inner = inner
def __call__(self, scope):
headers = dict(scope['headers'])
auth_header = None
if b'authorization' in headers:
auth_header = headers[b'authorization'].decode()
else:
try:
auth_header = _str_to_dict(headers[b'cookie'].decode())['X-Authorization']
except:
pass
logger.info(auth_header)
if auth_header:
try:
user_jwt = jwt.decode(
auth_header,
settings.SECRET_KEY,
)
scope['user'] = MyUser.objects.get(
id=user_jwt['user_id']
)
except (InvalidSignatureError, KeyError, ExpiredSignatureError, DecodeError):
traceback.print_exc()
pass
except Exception as e: # NoQA
logger.error(scope)
traceback.print_exc()
return self.inner(scope)
TokenAuthMiddlewareStack = lambda inner: TokenAuthMiddleware(AuthMiddlewareStack(inner))
def _str_to_dict(str):
return {k: v.strip('"') for k, v in re.findall(r'(\S+)=(".*?"|\S+)', str)}
routing.py
application = ProtocolTypeRouter({
# (http->django views is added by default)
'websocket': TokenAuthMiddlewareStack(
URLRouter(
cmonitorserv.routing.websocket_urlpatterns
)
),
})
Wasn't able to find a solution using middleware.
For now solved by handling auth permissions in consumers.py
def _is_authenticated(self):
if hasattr(self.scope, 'auth_error'):
return False
if not self.scope['user'] or self.scope['user'] is AnonymousUser:
return False
return True
Another important thing which doesn't seem to be documented anywhere - to reject a connection with the custom error code, we need to accept it first.
class WebConsumer(WebsocketConsumer):
def connect(self):
self.accept()
if self._is_authenticated():
....
else:
logger.error("ws client auth error")
self.close(code=4003)

How to test 500.html error page in django development env?

I am using Django for a project and is already in production.
In the production environment 500.html is rendered whenever a server error occurs.
How do I test the rendering of 500.html in dev environment? Or how do I render 500.html in dev, if I turn-off debug I still get the errors and not 500.html
background: I include some page elements based on a page and some are missing when 500.html is called and want to debug it in dev environment.
I prefer not to turn DEBUG off. Instead I put the following snippet in the urls.py:
if settings.DEBUG:
urlpatterns += patterns('',
(r'^500/$', 'your_custom_view_if_you_wrote_one'),
(r'^404/$', 'django.views.generic.simple.direct_to_template', {'template': '404.html'}),
)
In the snippet above, the error page uses a custom view, you can easily replace it with Django's direct_to_template view though.
Now you can test 500 and 404 pages by calling their urls: http://example.com/500 and http://example.com/404
In Django 1.6 django.views.generic.simple.direct_to_template does not exists anymore, these are my settings for special views:
# urls.py
from django.views.generic import TemplateView
from django.views.defaults import page_not_found, server_error
urlpatterns += [
url(r'^400/$', TemplateView.as_view(template_name='400.html')),
url(r'^403/$', TemplateView.as_view(template_name='403.html')),
url(r'^404/$', page_not_found),
url(r'^500/$', server_error),
]
And if you want to use the default Django 500 view instead of your custom view:
if settings.DEBUG:
urlpatterns += patterns('',
(r'^500/$', 'django.views.defaults.server_error'),
(r'^404/$', 'django.views.generic.simple.direct_to_template', {'template': '404.html'}),
)
Continuing shanyu's answer, in Django 1.3+ use:
if settings.DEBUG:
urlpatterns += patterns('',
(r'^500/$', 'django.views.defaults.server_error'),
(r'^404/$', 'django.views.defaults.page_not_found'),
)
For Django > 3.0, just set the raise_request_exception value to False.
from django.test import TestCase
class ViewTestClass(TestCase):
def test_error_page(self):
self.client.raise_request_exception = False
response = self.client.get(reverse('error-page'))
self.assertEqual(response.status_code, 500)
self.assertTrue(
'some text from the custom 500 page'
in response.content.decode('utf8'))
Documentation: https://docs.djangoproject.com/en/3.2/topics/testing/tools/
NOTE: if the error page raises an exception, that will show up as an ERROR in the test log. You can turn the test logging up to CRITICAL by default to suppress that error.
Are both debug settings false?
settings.DEBUG = False
settings.TEMPLATE_DEBUG = False
How i do and test custom error handlers
Define custom View based on TemplateView
# views.py
from django.views.generic import TemplateView
class ErrorHandler(TemplateView):
""" Render error template """
error_code = 404
template_name = 'index/error.html'
def dispatch(self, request, *args, **kwargs):
""" For error on any methods return just GET """
return self.get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['error_code'] = self.error_code
return context
def render_to_response(self, context, **response_kwargs):
""" Return correct status code """
response_kwargs = response_kwargs or {}
response_kwargs.update(status=self.error_code)
return super().render_to_response(context, **response_kwargs)
Tell django to use custom error handlers
# urls.py
from index.views import ErrorHandler
# error handing handlers - fly binding
for code in (400, 403, 404, 500):
vars()['handler{}'.format(code)] = ErrorHandler.as_view(error_code=code)
Testcase for custom error handlers
# tests.py
from unittest import mock
from django.test import TestCase
from django.core.exceptions import SuspiciousOperation, PermissionDenied
from django.http import Http404
from index import views
class ErrorHandlersTestCase(TestCase):
""" Check is correct error handlers work """
def raise_(exception):
def wrapped(*args, **kwargs):
raise exception('Test exception')
return wrapped
def test_index_page(self):
""" Should check is 200 on index page """
response = self.client.get('/')
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'index/index.html')
#mock.patch('index.views.IndexView.get', raise_(Http404))
def test_404_page(self):
""" Should check is 404 page correct """
response = self.client.get('/')
self.assertEqual(response.status_code, 404)
self.assertTemplateUsed(response, 'index/error.html')
self.assertIn('404 Page not found', response.content.decode('utf-8'))
#mock.patch('index.views.IndexView.get', views.ErrorHandler.as_view(error_code=500))
def test_500_page(self):
""" Should check is 500 page correct """
response = self.client.get('/')
self.assertEqual(response.status_code, 500)
self.assertTemplateUsed(response, 'index/error.html')
self.assertIn('500 Server Error', response.content.decode('utf-8'))
#mock.patch('index.views.IndexView.get', raise_(SuspiciousOperation))
def test_400_page(self):
""" Should check is 400 page correct """
response = self.client.get('/')
self.assertEqual(response.status_code, 400)
self.assertTemplateUsed(response, 'index/error.html')
self.assertIn('400 Bad request', response.content.decode('utf-8'))
#mock.patch('index.views.IndexView.get', raise_(PermissionDenied))
def test_403_page(self):
""" Should check is 403 page correct """
response = self.client.get('/')
self.assertEqual(response.status_code, 403)
self.assertTemplateUsed(response, 'index/error.html')
self.assertIn('403 Permission Denied', response.content.decode('utf-8'))
urls.py
handler500 = 'project.apps.core.views.handler500'
handler404 = 'project.apps.core.views.handler404'
views.py
from django.template.loader import get_template
from django.template import Context
from django.http import HttpResponseServerError, HttpResponseNotFound
def handler500(request, template_name='500.html'):
t = get_template(template_name)
ctx = Context({})
return HttpResponseServerError(t.render(ctx))
def handler404(request, template_name='404.html'):
t = get_template(template_name)
ctx = Context({})
return HttpResponseNotFound(t.render(ctx))
tests.py
from django.test import TestCase
from django.test.client import RequestFactory
from project import urls
from ..views import handler404, handler500
class TestErrorPages(TestCase):
def test_error_handlers(self):
self.assertTrue(urls.handler404.endswith('.handler404'))
self.assertTrue(urls.handler500.endswith('.handler500'))
factory = RequestFactory()
request = factory.get('/')
response = handler404(request)
self.assertEqual(response.status_code, 404)
self.assertIn('404 Not Found!!', unicode(response))
response = handler500(request)
self.assertEqual(response.status_code, 500)
self.assertIn('500 Internal Server Error', unicode(response))
Update for Django > 1.6 and without getting
page_not_found() missing 1 required positional argument: 'exception'
Inspired by this answer:
# urls.py
from django.views.defaults import page_not_found, server_error, permission_denied, bad_request
[...]
if settings.DEBUG:
# This allows the error pages to be debugged during development, just visit
# these url in browser to see how these error pages look like.
urlpatterns += [
path('400/', bad_request, kwargs={'exception': Exception('Bad Request!')}),
path('403/', permission_denied, kwargs={'exception': Exception('Permission Denied')}),
path('404/', page_not_found, kwargs={'exception': Exception('Page not Found')}),
path('500/', server_error),
You can simply define the handler404 and handler500 for errors in your main views.py file as detailed in this answer:
https://stackoverflow.com/a/18009660/1913888
This will return the error that you desire when Django routes to that handler. No custom URL configuration is needed to route to a different URL name.
In Django versions < 3.0, you should do as follows:
client.py
from django.core.signals import got_request_exception
from django.template import TemplateDoesNotExist
from django.test import signals
from django.test.client import Client as DjangoClient, store_rendered_templates
from django.urls import resolve
from django.utils import six
from django.utils.functional import SimpleLazyObject, curry
class Client(DjangoClient):
"""Test client that does not raise Exceptions if requested."""
def __init__(self,
enforce_csrf_checks=False,
raise_request_exception=True, **defaults):
super(Client, self).__init__(enforce_csrf_checks=enforce_csrf_checks,
**defaults)
self.raise_request_exception = raise_request_exception
def request(self, **request):
"""
The master request method. Composes the environment dictionary
and passes to the handler, returning the result of the handler.
Assumes defaults for the query environment, which can be overridden
using the arguments to the request.
"""
environ = self._base_environ(**request)
# Curry a data dictionary into an instance of the template renderer
# callback function.
data = {}
on_template_render = curry(store_rendered_templates, data)
signal_uid = "template-render-%s" % id(request)
signals.template_rendered.connect(on_template_render,
dispatch_uid=signal_uid)
# Capture exceptions created by the handler.
exception_uid = "request-exception-%s" % id(request)
got_request_exception.connect(self.store_exc_info,
dispatch_uid=exception_uid)
try:
try:
response = self.handler(environ)
except TemplateDoesNotExist as e:
# If the view raises an exception, Django will attempt to show
# the 500.html template. If that template is not available,
# we should ignore the error in favor of re-raising the
# underlying exception that caused the 500 error. Any other
# template found to be missing during view error handling
# should be reported as-is.
if e.args != ('500.html',):
raise
# Look for a signalled exception, clear the current context
# exception data, then re-raise the signalled exception.
# Also make sure that the signalled exception is cleared from
# the local cache!
response.exc_info = self.exc_info # Patch exception handling
if self.exc_info:
exc_info = self.exc_info
self.exc_info = None
if self.raise_request_exception: # Patch exception handling
six.reraise(*exc_info)
# Save the client and request that stimulated the response.
response.client = self
response.request = request
# Add any rendered template detail to the response.
response.templates = data.get("templates", [])
response.context = data.get("context")
response.json = curry(self._parse_json, response)
# Attach the ResolverMatch instance to the response
response.resolver_match = SimpleLazyObject(
lambda: resolve(request['PATH_INFO'])
)
# Flatten a single context. Not really necessary anymore thanks to
# the __getattr__ flattening in ContextList, but has some edge-case
# backwards-compatibility implications.
if response.context and len(response.context) == 1:
response.context = response.context[0]
# Update persistent cookie data.
if response.cookies:
self.cookies.update(response.cookies)
return response
finally:
signals.template_rendered.disconnect(dispatch_uid=signal_uid)
got_request_exception.disconnect(dispatch_uid=exception_uid)
tests.py
from unittest import mock
from django.contrib.auth import get_user_model
from django.core.urlresolvers import reverse
from django.test import TestCase, override_settings
from .client import Client # Important, we use our own Client here!
class TestErrors(TestCase):
"""Test errors."""
#classmethod
def setUpClass(cls):
super(TestErrors, cls).setUpClass()
cls.username = 'admin'
cls.email = 'admin#localhost'
cls.password = 'test1234test1234'
cls.not_found_url = '/i-do-not-exist/'
cls.internal_server_error_url = reverse('password_reset')
def setUp(self):
super(TestErrors, self).setUp()
User = get_user_model()
User.objects.create_user(
self.username,
self.email,
self.password,
is_staff=True,
is_active=True
)
self.client = Client(raise_request_exception=False)
# Mock in order to trigger Exception and resulting Internal server error
#mock.patch('django.contrib.auth.views.PasswordResetView.form_class', None)
#override_settings(DEBUG=False)
def test_errors(self):
self.client.login(username=self.username, password=self.password)
with self.subTest("Not found (404)"):
response = self.client.get(self.not_found_url, follow=True)
self.assertNotIn('^admin/', str(response.content))
with self.subTest("Internal server error (500)"):
response = self.client.get(self.internal_server_error_url,
follow=True)
self.assertNotIn('TypeError', str(response.content))
Starting from Django 3.0 you could skip the custom Client definition and just use the code from tests.py.