summaryrefslogtreecommitdiff
path: root/paste/exceptions/errormiddleware.py
diff options
context:
space:
mode:
Diffstat (limited to 'paste/exceptions/errormiddleware.py')
-rw-r--r--paste/exceptions/errormiddleware.py466
1 files changed, 466 insertions, 0 deletions
diff --git a/paste/exceptions/errormiddleware.py b/paste/exceptions/errormiddleware.py
new file mode 100644
index 0000000..95c1261
--- /dev/null
+++ b/paste/exceptions/errormiddleware.py
@@ -0,0 +1,466 @@
+# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
+# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
+
+"""
+Error handler middleware
+"""
+import sys
+import traceback
+import cgi
+from six.moves import cStringIO as StringIO
+from paste.exceptions import formatter, collector, reporter
+from paste import wsgilib
+from paste import request
+import six
+
+__all__ = ['ErrorMiddleware', 'handle_exception']
+
+class _NoDefault(object):
+ def __repr__(self):
+ return '<NoDefault>'
+NoDefault = _NoDefault()
+
+class ErrorMiddleware(object):
+
+ """
+ Error handling middleware
+
+ Usage::
+
+ error_catching_wsgi_app = ErrorMiddleware(wsgi_app)
+
+ Settings:
+
+ ``debug``:
+ If true, then tracebacks will be shown in the browser.
+
+ ``error_email``:
+ an email address (or list of addresses) to send exception
+ reports to
+
+ ``error_log``:
+ a filename to append tracebacks to
+
+ ``show_exceptions_in_wsgi_errors``:
+ If true, then errors will be printed to ``wsgi.errors``
+ (frequently a server error log, or stderr).
+
+ ``from_address``, ``smtp_server``, ``error_subject_prefix``, ``smtp_username``, ``smtp_password``, ``smtp_use_tls``:
+ variables to control the emailed exception reports
+
+ ``error_message``:
+ When debug mode is off, the error message to show to users.
+
+ ``xmlhttp_key``:
+ When this key (default ``_``) is in the request GET variables
+ (not POST!), expect that this is an XMLHttpRequest, and the
+ response should be more minimal; it should not be a complete
+ HTML page.
+
+ Environment Configuration:
+
+ ``paste.throw_errors``:
+ If this setting in the request environment is true, then this
+ middleware is disabled. This can be useful in a testing situation
+ where you don't want errors to be caught and transformed.
+
+ ``paste.expected_exceptions``:
+ When this middleware encounters an exception listed in this
+ environment variable and when the ``start_response`` has not
+ yet occurred, the exception will be re-raised instead of being
+ caught. This should generally be set by middleware that may
+ (but probably shouldn't be) installed above this middleware,
+ and wants to get certain exceptions. Exceptions raised after
+ ``start_response`` have been called are always caught since
+ by definition they are no longer expected.
+
+ """
+
+ def __init__(self, application, global_conf=None,
+ debug=NoDefault,
+ error_email=None,
+ error_log=None,
+ show_exceptions_in_wsgi_errors=NoDefault,
+ from_address=None,
+ smtp_server=None,
+ smtp_username=None,
+ smtp_password=None,
+ smtp_use_tls=False,
+ error_subject_prefix=None,
+ error_message=None,
+ xmlhttp_key=None):
+ from paste.util import converters
+ self.application = application
+ # @@: global_conf should be handled elsewhere in a separate
+ # function for the entry point
+ if global_conf is None:
+ global_conf = {}
+ if debug is NoDefault:
+ debug = converters.asbool(global_conf.get('debug'))
+ if show_exceptions_in_wsgi_errors is NoDefault:
+ show_exceptions_in_wsgi_errors = converters.asbool(global_conf.get('show_exceptions_in_wsgi_errors'))
+ self.debug_mode = converters.asbool(debug)
+ if error_email is None:
+ error_email = (global_conf.get('error_email')
+ or global_conf.get('admin_email')
+ or global_conf.get('webmaster_email')
+ or global_conf.get('sysadmin_email'))
+ self.error_email = converters.aslist(error_email)
+ self.error_log = error_log
+ self.show_exceptions_in_wsgi_errors = show_exceptions_in_wsgi_errors
+ if from_address is None:
+ from_address = global_conf.get('error_from_address', 'errors@localhost')
+ self.from_address = from_address
+ if smtp_server is None:
+ smtp_server = global_conf.get('smtp_server', 'localhost')
+ self.smtp_server = smtp_server
+ self.smtp_username = smtp_username or global_conf.get('smtp_username')
+ self.smtp_password = smtp_password or global_conf.get('smtp_password')
+ self.smtp_use_tls = smtp_use_tls or converters.asbool(global_conf.get('smtp_use_tls'))
+ self.error_subject_prefix = error_subject_prefix or ''
+ if error_message is None:
+ error_message = global_conf.get('error_message')
+ self.error_message = error_message
+ if xmlhttp_key is None:
+ xmlhttp_key = global_conf.get('xmlhttp_key', '_')
+ self.xmlhttp_key = xmlhttp_key
+
+ def __call__(self, environ, start_response):
+ """
+ The WSGI application interface.
+ """
+ # We want to be careful about not sending headers twice,
+ # and the content type that the app has committed to (if there
+ # is an exception in the iterator body of the response)
+ if environ.get('paste.throw_errors'):
+ return self.application(environ, start_response)
+ environ['paste.throw_errors'] = True
+
+ try:
+ __traceback_supplement__ = Supplement, self, environ
+ sr_checker = ResponseStartChecker(start_response)
+ app_iter = self.application(environ, sr_checker)
+ return self.make_catching_iter(app_iter, environ, sr_checker)
+ except:
+ exc_info = sys.exc_info()
+ try:
+ for expect in environ.get('paste.expected_exceptions', []):
+ if isinstance(exc_info[1], expect):
+ raise
+ start_response('500 Internal Server Error',
+ [('content-type', 'text/html')],
+ exc_info)
+ # @@: it would be nice to deal with bad content types here
+ response = self.exception_handler(exc_info, environ)
+ if six.PY3:
+ response = response.encode('utf8')
+ return [response]
+ finally:
+ # clean up locals...
+ exc_info = None
+
+ def make_catching_iter(self, app_iter, environ, sr_checker):
+ if isinstance(app_iter, (list, tuple)):
+ # These don't raise
+ return app_iter
+ return CatchingIter(app_iter, environ, sr_checker, self)
+
+ def exception_handler(self, exc_info, environ):
+ simple_html_error = False
+ if self.xmlhttp_key:
+ get_vars = request.parse_querystring(environ)
+ if dict(get_vars).get(self.xmlhttp_key):
+ simple_html_error = True
+ return handle_exception(
+ exc_info, environ['wsgi.errors'],
+ html=True,
+ debug_mode=self.debug_mode,
+ error_email=self.error_email,
+ error_log=self.error_log,
+ show_exceptions_in_wsgi_errors=self.show_exceptions_in_wsgi_errors,
+ error_email_from=self.from_address,
+ smtp_server=self.smtp_server,
+ smtp_username=self.smtp_username,
+ smtp_password=self.smtp_password,
+ smtp_use_tls=self.smtp_use_tls,
+ error_subject_prefix=self.error_subject_prefix,
+ error_message=self.error_message,
+ simple_html_error=simple_html_error)
+
+class ResponseStartChecker(object):
+ def __init__(self, start_response):
+ self.start_response = start_response
+ self.response_started = False
+
+ def __call__(self, *args):
+ self.response_started = True
+ self.start_response(*args)
+
+class CatchingIter(object):
+
+ """
+ A wrapper around the application iterator that will catch
+ exceptions raised by the a generator, or by the close method, and
+ display or report as necessary.
+ """
+
+ def __init__(self, app_iter, environ, start_checker, error_middleware):
+ self.app_iterable = app_iter
+ self.app_iterator = iter(app_iter)
+ self.environ = environ
+ self.start_checker = start_checker
+ self.error_middleware = error_middleware
+ self.closed = False
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ __traceback_supplement__ = (
+ Supplement, self.error_middleware, self.environ)
+ if self.closed:
+ raise StopIteration
+ try:
+ return self.app_iterator.next()
+ except StopIteration:
+ self.closed = True
+ close_response = self._close()
+ if close_response is not None:
+ return close_response
+ else:
+ raise StopIteration
+ except:
+ self.closed = True
+ close_response = self._close()
+ exc_info = sys.exc_info()
+ response = self.error_middleware.exception_handler(
+ exc_info, self.environ)
+ if close_response is not None:
+ response += (
+ '<hr noshade>Error in .close():<br>%s'
+ % close_response)
+
+ if not self.start_checker.response_started:
+ self.start_checker('500 Internal Server Error',
+ [('content-type', 'text/html')],
+ exc_info)
+
+ if six.PY3:
+ response = response.encode('utf8')
+ return response
+ __next__ = next
+
+ def close(self):
+ # This should at least print something to stderr if the
+ # close method fails at this point
+ if not self.closed:
+ self._close()
+
+ def _close(self):
+ """Close and return any error message"""
+ if not hasattr(self.app_iterable, 'close'):
+ return None
+ try:
+ self.app_iterable.close()
+ return None
+ except:
+ close_response = self.error_middleware.exception_handler(
+ sys.exc_info(), self.environ)
+ return close_response
+
+
+class Supplement(object):
+
+ """
+ This is a supplement used to display standard WSGI information in
+ the traceback.
+ """
+
+ def __init__(self, middleware, environ):
+ self.middleware = middleware
+ self.environ = environ
+ self.source_url = request.construct_url(environ)
+
+ def extraData(self):
+ data = {}
+ cgi_vars = data[('extra', 'CGI Variables')] = {}
+ wsgi_vars = data[('extra', 'WSGI Variables')] = {}
+ hide_vars = ['paste.config', 'wsgi.errors', 'wsgi.input',
+ 'wsgi.multithread', 'wsgi.multiprocess',
+ 'wsgi.run_once', 'wsgi.version',
+ 'wsgi.url_scheme']
+ for name, value in self.environ.items():
+ if name.upper() == name:
+ if value:
+ cgi_vars[name] = value
+ elif name not in hide_vars:
+ wsgi_vars[name] = value
+ if self.environ['wsgi.version'] != (1, 0):
+ wsgi_vars['wsgi.version'] = self.environ['wsgi.version']
+ proc_desc = tuple([int(bool(self.environ[key]))
+ for key in ('wsgi.multiprocess',
+ 'wsgi.multithread',
+ 'wsgi.run_once')])
+ wsgi_vars['wsgi process'] = self.process_combos[proc_desc]
+ wsgi_vars['application'] = self.middleware.application
+ if 'paste.config' in self.environ:
+ data[('extra', 'Configuration')] = dict(self.environ['paste.config'])
+ return data
+
+ process_combos = {
+ # multiprocess, multithread, run_once
+ (0, 0, 0): 'Non-concurrent server',
+ (0, 1, 0): 'Multithreaded',
+ (1, 0, 0): 'Multiprocess',
+ (1, 1, 0): 'Multi process AND threads (?)',
+ (0, 0, 1): 'Non-concurrent CGI',
+ (0, 1, 1): 'Multithread CGI (?)',
+ (1, 0, 1): 'CGI',
+ (1, 1, 1): 'Multi thread/process CGI (?)',
+ }
+
+def handle_exception(exc_info, error_stream, html=True,
+ debug_mode=False,
+ error_email=None,
+ error_log=None,
+ show_exceptions_in_wsgi_errors=False,
+ error_email_from='errors@localhost',
+ smtp_server='localhost',
+ smtp_username=None,
+ smtp_password=None,
+ smtp_use_tls=False,
+ error_subject_prefix='',
+ error_message=None,
+ simple_html_error=False,
+ ):
+ """
+ For exception handling outside of a web context
+
+ Use like::
+
+ import sys
+ from paste.exceptions.errormiddleware import handle_exception
+ try:
+ do stuff
+ except:
+ handle_exception(
+ sys.exc_info(), sys.stderr, html=False, ...other config...)
+
+ If you want to report, but not fully catch the exception, call
+ ``raise`` after ``handle_exception``, which (when given no argument)
+ will reraise the exception.
+ """
+ reported = False
+ exc_data = collector.collect_exception(*exc_info)
+ extra_data = ''
+ if error_email:
+ rep = reporter.EmailReporter(
+ to_addresses=error_email,
+ from_address=error_email_from,
+ smtp_server=smtp_server,
+ smtp_username=smtp_username,
+ smtp_password=smtp_password,
+ smtp_use_tls=smtp_use_tls,
+ subject_prefix=error_subject_prefix)
+ rep_err = send_report(rep, exc_data, html=html)
+ if rep_err:
+ extra_data += rep_err
+ else:
+ reported = True
+ if error_log:
+ rep = reporter.LogReporter(
+ filename=error_log)
+ rep_err = send_report(rep, exc_data, html=html)
+ if rep_err:
+ extra_data += rep_err
+ else:
+ reported = True
+ if show_exceptions_in_wsgi_errors:
+ rep = reporter.FileReporter(
+ file=error_stream)
+ rep_err = send_report(rep, exc_data, html=html)
+ if rep_err:
+ extra_data += rep_err
+ else:
+ reported = True
+ else:
+ line = ('Error - %s: %s\n'
+ % (exc_data.exception_type, exc_data.exception_value))
+ if six.PY3:
+ line = line.encode('utf8')
+ error_stream.write(line)
+ if html:
+ if debug_mode and simple_html_error:
+ return_error = formatter.format_html(
+ exc_data, include_hidden_frames=False,
+ include_reusable=False, show_extra_data=False)
+ reported = True
+ elif debug_mode and not simple_html_error:
+ error_html = formatter.format_html(
+ exc_data,
+ include_hidden_frames=True,
+ include_reusable=False)
+ head_html = formatter.error_css + formatter.hide_display_js
+ return_error = error_template(
+ head_html, error_html, extra_data)
+ extra_data = ''
+ reported = True
+ else:
+ msg = error_message or '''
+ An error occurred. See the error logs for more information.
+ (Turn debug on to display exception reports here)
+ '''
+ return_error = error_template('', msg, '')
+ else:
+ return_error = None
+ if not reported and error_stream:
+ err_report = formatter.format_text(exc_data, show_hidden_frames=True)
+ err_report += '\n' + '-'*60 + '\n'
+ error_stream.write(err_report)
+ if extra_data:
+ error_stream.write(extra_data)
+ return return_error
+
+def send_report(rep, exc_data, html=True):
+ try:
+ rep.report(exc_data)
+ except:
+ output = StringIO()
+ traceback.print_exc(file=output)
+ if html:
+ return """
+ <p>Additionally an error occurred while sending the %s report:
+
+ <pre>%s</pre>
+ </p>""" % (
+ cgi.escape(str(rep)), output.getvalue())
+ else:
+ return (
+ "Additionally an error occurred while sending the "
+ "%s report:\n%s" % (str(rep), output.getvalue()))
+ else:
+ return ''
+
+def error_template(head_html, exception, extra):
+ return '''
+ <html>
+ <head>
+ <title>Server Error</title>
+ %s
+ </head>
+ <body>
+ <h1>Server Error</h1>
+ %s
+ %s
+ </body>
+ </html>''' % (head_html, exception, extra)
+
+def make_error_middleware(app, global_conf, **kw):
+ return ErrorMiddleware(app, global_conf=global_conf, **kw)
+
+doc_lines = ErrorMiddleware.__doc__.splitlines(True)
+for i in range(len(doc_lines)):
+ if doc_lines[i].strip().startswith('Settings'):
+ make_error_middleware.__doc__ = ''.join(doc_lines[i:])
+ break
+del i, doc_lines