From 6df753de36816d0aa63d9bfa3f735b79bdb786e0 Mon Sep 17 00:00:00 2001 From: Will Kahn-Greene Date: Wed, 4 Mar 2015 10:35:52 -0500 Subject: [PATCH 1/3] Cosmetic: Minor cleanup of server error testing I mostly just moved some things around so they're in places that are more logical. --- fjord/base/tests/test_views.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/fjord/base/tests/test_views.py b/fjord/base/tests/test_views.py index dcc59650..5e25b474 100644 --- a/fjord/base/tests/test_views.py +++ b/fjord/base/tests/test_views.py @@ -79,15 +79,8 @@ def connect(self): finally: views.test_memcached = test_memcached - @override_settings(SHOW_STAGE_NOTICE=True) - def test_500(self): - with self.assertRaises(IntentionalException) as cm: - self.client.get('/services/throw-error') - eq_(type(cm.exception), IntentionalException) - - -class ErrorTesting(ElasticTestCase): +class FileNotFoundTesting(TestCase): client_class = LocalizingClient def test_404(self): @@ -96,6 +89,15 @@ def test_404(self): self.assertTemplateUsed(request, '404.html') +class ServerErrorTesting(TestCase): + @override_settings(SHOW_STAGE_NOTICE=True) + def test_500(self): + with self.assertRaises(IntentionalException) as cm: + self.client.get('/services/throw-error') + + eq_(type(cm.exception), IntentionalException) + + class TestRobots(TestCase): def test_robots(self): resp = self.client.get('/robots.txt') From 1dcaf62513c5dab1893e510b87454fd000c65126 Mon Sep 17 00:00:00 2001 From: Will Kahn-Greene Date: Wed, 4 Mar 2015 12:36:33 -0500 Subject: [PATCH 2/3] Make throw_error csrf exempt --- fjord/base/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fjord/base/views.py b/fjord/base/views.py index 40e79c30..8b2b8219 100644 --- a/fjord/base/views.py +++ b/fjord/base/views.py @@ -12,6 +12,7 @@ from django.shortcuts import render from django.utils.http import is_safe_url from django.views.decorators.cache import never_cache +from django.views.decorators.csrf import csrf_exempt from celery.messaging import establish_connection from elasticsearch.exceptions import ConnectionError, NotFoundError @@ -228,6 +229,7 @@ class IntentionalException(Exception): @dev_or_authorized +@csrf_exempt def throw_error(request): """Throw an error for testing purposes.""" raise IntentionalException("Error raised for testing purposes.") From ceb53eb348c775b3b996dc0e2514c6bb23011eaa Mon Sep 17 00:00:00 2001 From: Will Kahn-Greene Date: Wed, 4 Mar 2015 09:49:17 -0500 Subject: [PATCH 3/3] [bug 1136840] Fix error handling for better debugging * creates DebuggableWSGIHandler which should send more useful error emails so I can debug API-related problems, plus in DEBUG=True mode, it'll return plain formatted content rather than html formatted content; plain text is a lot easier to read in a terminal when debugging API-related problems * created fjord/wsgi.py which gets used by "./manage.py runserver" and holds common WSGI setup for fjord * adjusts wsgi/playdoh.wsgi which gets used by stage/prod environments to do stage/prod specific things and then use fjord/wsgi.py for the rest --- fjord/settings/base.py | 1 + fjord/wsgi.py | 30 +++++++++++++++ fjord/wsgi_utils.py | 87 ++++++++++++++++++++++++++++++++++++++++++ wsgi/playdoh.wsgi | 25 ++++++++---- 4 files changed, 135 insertions(+), 8 deletions(-) create mode 100644 fjord/wsgi.py create mode 100644 fjord/wsgi_utils.py diff --git a/fjord/settings/base.py b/fjord/settings/base.py index d1177e7a..2bf16471 100644 --- a/fjord/settings/base.py +++ b/fjord/settings/base.py @@ -581,6 +581,7 @@ def JINJA_CONFIG(): ] } +WSGI_APPLICATION = 'fjord.wsgi.application' # When set to True, this will cause a message to be displayed on all # pages that this is not production. diff --git a/fjord/wsgi.py b/fjord/wsgi.py new file mode 100644 index 00000000..29b6f289 --- /dev/null +++ b/fjord/wsgi.py @@ -0,0 +1,30 @@ +# This file gets run by ./manage.py (runserver|test) and also gets +# imported for the WSGI application building by wsgi/playdoh.wsgi +# for stage/prod. +# +# It holds the setup that's common to both environments. + +import os + +import django +from django.core.handlers.wsgi import WSGIHandler + +from fjord.wsgi_utils import BetterDebugMixin + + +os.environ.setdefault('CELERY_LOADER', 'django') + + +class DebuggableWSGIHandler(BetterDebugMixin, WSGIHandler): + pass + + +def get_debuggable_wsgi_application(): + # This does the same thing as + # django.core.wsgi.get_wsgi_application except it returns a + # different WSGIHandler. + django.setup() + return DebuggableWSGIHandler() + + +application = get_debuggable_wsgi_application() diff --git a/fjord/wsgi_utils.py b/fjord/wsgi_utils.py new file mode 100644 index 00000000..f1c764fb --- /dev/null +++ b/fjord/wsgi_utils.py @@ -0,0 +1,87 @@ +from django.conf import settings + + +class BetterDebugMixin(object): + """Provides better debugging data + + Developing API endpoints and tired of wading through HTML for HTTP + 500 errors? + + Working on POST API debugging and not seeing the POST data show up + in the error logs/emails? + + Then this mixin is for you! + + It: + + * spits out text rather than html when DEBUG = True (OMG! THANK + YOU!) + * adds a "HTTP_X_POSTBODY" META variable so you can see the raw post + data in error emails which is gross, but I couldn't figure out + a better way to do it + + Usage: + + Create a WSGIHandler subclass and bind that to ``application`` in + your wsgi file. For example:: + + import django + from django.core.handlers.wsgi import WSGIHandler + + from fjord.wsgi_utils import BetterDebugMixin + + + class MyWSGIHandler(BetterDebugMixin, WSGIHandler): + pass + + + def get_debuggable_wsgi_application(): + # This does the same thing as + # django.core.wsgi.get_wsgi_application except + # it returns a different WSGIHandler. + django.setup() + return MyWSGIHandler() + + + application = get_debuggable_wsgi_application() + + """ + def handle_uncaught_exception(self, request, resolver, exc_info): + if settings.DEBUG_PROPAGATE_EXCEPTIONS: + raise + + # First, grab the raw POST body and put it somewhere that's + # guaranteed to show up in the error email. + + # This should be "bytes" which is str type in Python 2. + # + # FIXME: This is probably broken with Python 3. + postbody = getattr(request, 'body', '') + try: + # For string-ish data, we truncate and decode/re-encode in + # utf-8. + postbody = postbody[:10000].decode('utf-8').encode('utf-8') + except (UnicodeDecodeError, UnicodeEncodeError): + # For binary, we say, 'BINARY CONTENT' + postbody = 'BINARY OR NON-UTF-8 CONTENT' + + # The logger.error generates a record which can get handled by + # the AdminEmailHandler. Overriding all that machinery is + # daunting, so we're instead going to shove it in the META + # section which shows up when the machinery does a repr on + # WSGIRequest. + request.META['HTTP_X_POST_BODY'] = postbody + + # Second, check the Accept header and if it's not text/html, + # pretend this is an AJAX request so that we get the output in + # text rather than html when DEBUG=True. + if settings.DEBUG: + # request.is_ajax() == True will push this into doing text + # instead of html which is waaaaaayyy more useful from an + # API perspective. So if the Accept header is anything other + # than html, we'll say it's an ajax request to return text. + if 'html' not in request.META.get('HTTP_ACCEPT', 'text/html'): + request.META['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' + + return super(BetterDebugMixin, self).handle_uncaught_exception( + request, resolver, exc_info) diff --git a/wsgi/playdoh.wsgi b/wsgi/playdoh.wsgi index b7649b1a..c6c36c80 100644 --- a/wsgi/playdoh.wsgi +++ b/wsgi/playdoh.wsgi @@ -1,6 +1,11 @@ +# This gets used by stage/prod to set up the WSGI application for stage/prod +# use. We do some minor environment setup and then have `fjord/wsgi.py` do +# the rest. + import os import site +# Set up NewRelic stuff. try: import newrelic.agent except ImportError: @@ -15,7 +20,6 @@ if newrelic: newrelic = False -os.environ.setdefault('CELERY_LOADER', 'django') # NOTE: you can also set DJANGO_SETTINGS_MODULE in your environment to override # the default value in manage.py @@ -24,18 +28,23 @@ wsgidir = os.path.dirname(__file__) site.addsitedir(os.path.abspath(os.path.join(wsgidir, '../'))) # Explicitly set these so that fjord.manage_utils does the right -# thing. +# thing in production. os.environ['USING_VENDOR'] = '1' os.environ['SKIP_CHECK'] = '1' -# manage adds vendor to the Python path and otherwise sets up the -# environment. +# Importing manage has the side-effect of adding vendor/ stuff and +# doing other environment setup. import manage -from django.core.wsgi import get_wsgi_application -application = get_wsgi_application() + +# This is the original Django WSGIHandler preserved here in case +# we ever have to back out the debuggable one. +# from django.core.wsgi import get_wsgi_application +# application = get_wsgi_application() + +from fjord.wsgi import get_debuggable_wsgi_application +application = get_debuggable_wsgi_application() + if newrelic: application = newrelic.agent.wsgi_application()(application) - -# vim: ft=python