1# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) 2# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php 3 4""" 5Error handler middleware 6""" 7import sys 8import traceback 9import cgi 10from six.moves import cStringIO as StringIO 11from paste.exceptions import formatter, collector, reporter 12from paste import wsgilib 13from paste import request 14import six 15 16__all__ = ['ErrorMiddleware', 'handle_exception'] 17 18class _NoDefault(object): 19 def __repr__(self): 20 return '<NoDefault>' 21NoDefault = _NoDefault() 22 23class ErrorMiddleware(object): 24 25 """ 26 Error handling middleware 27 28 Usage:: 29 30 error_catching_wsgi_app = ErrorMiddleware(wsgi_app) 31 32 Settings: 33 34 ``debug``: 35 If true, then tracebacks will be shown in the browser. 36 37 ``error_email``: 38 an email address (or list of addresses) to send exception 39 reports to 40 41 ``error_log``: 42 a filename to append tracebacks to 43 44 ``show_exceptions_in_wsgi_errors``: 45 If true, then errors will be printed to ``wsgi.errors`` 46 (frequently a server error log, or stderr). 47 48 ``from_address``, ``smtp_server``, ``error_subject_prefix``, ``smtp_username``, ``smtp_password``, ``smtp_use_tls``: 49 variables to control the emailed exception reports 50 51 ``error_message``: 52 When debug mode is off, the error message to show to users. 53 54 ``xmlhttp_key``: 55 When this key (default ``_``) is in the request GET variables 56 (not POST!), expect that this is an XMLHttpRequest, and the 57 response should be more minimal; it should not be a complete 58 HTML page. 59 60 Environment Configuration: 61 62 ``paste.throw_errors``: 63 If this setting in the request environment is true, then this 64 middleware is disabled. This can be useful in a testing situation 65 where you don't want errors to be caught and transformed. 66 67 ``paste.expected_exceptions``: 68 When this middleware encounters an exception listed in this 69 environment variable and when the ``start_response`` has not 70 yet occurred, the exception will be re-raised instead of being 71 caught. This should generally be set by middleware that may 72 (but probably shouldn't be) installed above this middleware, 73 and wants to get certain exceptions. Exceptions raised after 74 ``start_response`` have been called are always caught since 75 by definition they are no longer expected. 76 77 """ 78 79 def __init__(self, application, global_conf=None, 80 debug=NoDefault, 81 error_email=None, 82 error_log=None, 83 show_exceptions_in_wsgi_errors=NoDefault, 84 from_address=None, 85 smtp_server=None, 86 smtp_username=None, 87 smtp_password=None, 88 smtp_use_tls=False, 89 error_subject_prefix=None, 90 error_message=None, 91 xmlhttp_key=None): 92 from paste.util import converters 93 self.application = application 94 # @@: global_conf should be handled elsewhere in a separate 95 # function for the entry point 96 if global_conf is None: 97 global_conf = {} 98 if debug is NoDefault: 99 debug = converters.asbool(global_conf.get('debug')) 100 if show_exceptions_in_wsgi_errors is NoDefault: 101 show_exceptions_in_wsgi_errors = converters.asbool(global_conf.get('show_exceptions_in_wsgi_errors')) 102 self.debug_mode = converters.asbool(debug) 103 if error_email is None: 104 error_email = (global_conf.get('error_email') 105 or global_conf.get('admin_email') 106 or global_conf.get('webmaster_email') 107 or global_conf.get('sysadmin_email')) 108 self.error_email = converters.aslist(error_email) 109 self.error_log = error_log 110 self.show_exceptions_in_wsgi_errors = show_exceptions_in_wsgi_errors 111 if from_address is None: 112 from_address = global_conf.get('error_from_address', 'errors@localhost') 113 self.from_address = from_address 114 if smtp_server is None: 115 smtp_server = global_conf.get('smtp_server', 'localhost') 116 self.smtp_server = smtp_server 117 self.smtp_username = smtp_username or global_conf.get('smtp_username') 118 self.smtp_password = smtp_password or global_conf.get('smtp_password') 119 self.smtp_use_tls = smtp_use_tls or converters.asbool(global_conf.get('smtp_use_tls')) 120 self.error_subject_prefix = error_subject_prefix or '' 121 if error_message is None: 122 error_message = global_conf.get('error_message') 123 self.error_message = error_message 124 if xmlhttp_key is None: 125 xmlhttp_key = global_conf.get('xmlhttp_key', '_') 126 self.xmlhttp_key = xmlhttp_key 127 128 def __call__(self, environ, start_response): 129 """ 130 The WSGI application interface. 131 """ 132 # We want to be careful about not sending headers twice, 133 # and the content type that the app has committed to (if there 134 # is an exception in the iterator body of the response) 135 if environ.get('paste.throw_errors'): 136 return self.application(environ, start_response) 137 environ['paste.throw_errors'] = True 138 139 try: 140 __traceback_supplement__ = Supplement, self, environ 141 sr_checker = ResponseStartChecker(start_response) 142 app_iter = self.application(environ, sr_checker) 143 return self.make_catching_iter(app_iter, environ, sr_checker) 144 except: 145 exc_info = sys.exc_info() 146 try: 147 for expect in environ.get('paste.expected_exceptions', []): 148 if isinstance(exc_info[1], expect): 149 raise 150 start_response('500 Internal Server Error', 151 [('content-type', 'text/html')], 152 exc_info) 153 # @@: it would be nice to deal with bad content types here 154 response = self.exception_handler(exc_info, environ) 155 if six.PY3: 156 response = response.encode('utf8') 157 return [response] 158 finally: 159 # clean up locals... 160 exc_info = None 161 162 def make_catching_iter(self, app_iter, environ, sr_checker): 163 if isinstance(app_iter, (list, tuple)): 164 # These don't raise 165 return app_iter 166 return CatchingIter(app_iter, environ, sr_checker, self) 167 168 def exception_handler(self, exc_info, environ): 169 simple_html_error = False 170 if self.xmlhttp_key: 171 get_vars = request.parse_querystring(environ) 172 if dict(get_vars).get(self.xmlhttp_key): 173 simple_html_error = True 174 return handle_exception( 175 exc_info, environ['wsgi.errors'], 176 html=True, 177 debug_mode=self.debug_mode, 178 error_email=self.error_email, 179 error_log=self.error_log, 180 show_exceptions_in_wsgi_errors=self.show_exceptions_in_wsgi_errors, 181 error_email_from=self.from_address, 182 smtp_server=self.smtp_server, 183 smtp_username=self.smtp_username, 184 smtp_password=self.smtp_password, 185 smtp_use_tls=self.smtp_use_tls, 186 error_subject_prefix=self.error_subject_prefix, 187 error_message=self.error_message, 188 simple_html_error=simple_html_error) 189 190class ResponseStartChecker(object): 191 def __init__(self, start_response): 192 self.start_response = start_response 193 self.response_started = False 194 195 def __call__(self, *args): 196 self.response_started = True 197 self.start_response(*args) 198 199class CatchingIter(object): 200 201 """ 202 A wrapper around the application iterator that will catch 203 exceptions raised by the a generator, or by the close method, and 204 display or report as necessary. 205 """ 206 207 def __init__(self, app_iter, environ, start_checker, error_middleware): 208 self.app_iterable = app_iter 209 self.app_iterator = iter(app_iter) 210 self.environ = environ 211 self.start_checker = start_checker 212 self.error_middleware = error_middleware 213 self.closed = False 214 215 def __iter__(self): 216 return self 217 218 def next(self): 219 __traceback_supplement__ = ( 220 Supplement, self.error_middleware, self.environ) 221 if self.closed: 222 raise StopIteration 223 try: 224 return self.app_iterator.next() 225 except StopIteration: 226 self.closed = True 227 close_response = self._close() 228 if close_response is not None: 229 return close_response 230 else: 231 raise StopIteration 232 except: 233 self.closed = True 234 close_response = self._close() 235 exc_info = sys.exc_info() 236 response = self.error_middleware.exception_handler( 237 exc_info, self.environ) 238 if close_response is not None: 239 response += ( 240 '<hr noshade>Error in .close():<br>%s' 241 % close_response) 242 243 if not self.start_checker.response_started: 244 self.start_checker('500 Internal Server Error', 245 [('content-type', 'text/html')], 246 exc_info) 247 248 if six.PY3: 249 response = response.encode('utf8') 250 return response 251 __next__ = next 252 253 def close(self): 254 # This should at least print something to stderr if the 255 # close method fails at this point 256 if not self.closed: 257 self._close() 258 259 def _close(self): 260 """Close and return any error message""" 261 if not hasattr(self.app_iterable, 'close'): 262 return None 263 try: 264 self.app_iterable.close() 265 return None 266 except: 267 close_response = self.error_middleware.exception_handler( 268 sys.exc_info(), self.environ) 269 return close_response 270 271 272class Supplement(object): 273 274 """ 275 This is a supplement used to display standard WSGI information in 276 the traceback. 277 """ 278 279 def __init__(self, middleware, environ): 280 self.middleware = middleware 281 self.environ = environ 282 self.source_url = request.construct_url(environ) 283 284 def extraData(self): 285 data = {} 286 cgi_vars = data[('extra', 'CGI Variables')] = {} 287 wsgi_vars = data[('extra', 'WSGI Variables')] = {} 288 hide_vars = ['paste.config', 'wsgi.errors', 'wsgi.input', 289 'wsgi.multithread', 'wsgi.multiprocess', 290 'wsgi.run_once', 'wsgi.version', 291 'wsgi.url_scheme'] 292 for name, value in self.environ.items(): 293 if name.upper() == name: 294 if value: 295 cgi_vars[name] = value 296 elif name not in hide_vars: 297 wsgi_vars[name] = value 298 if self.environ['wsgi.version'] != (1, 0): 299 wsgi_vars['wsgi.version'] = self.environ['wsgi.version'] 300 proc_desc = tuple([int(bool(self.environ[key])) 301 for key in ('wsgi.multiprocess', 302 'wsgi.multithread', 303 'wsgi.run_once')]) 304 wsgi_vars['wsgi process'] = self.process_combos[proc_desc] 305 wsgi_vars['application'] = self.middleware.application 306 if 'paste.config' in self.environ: 307 data[('extra', 'Configuration')] = dict(self.environ['paste.config']) 308 return data 309 310 process_combos = { 311 # multiprocess, multithread, run_once 312 (0, 0, 0): 'Non-concurrent server', 313 (0, 1, 0): 'Multithreaded', 314 (1, 0, 0): 'Multiprocess', 315 (1, 1, 0): 'Multi process AND threads (?)', 316 (0, 0, 1): 'Non-concurrent CGI', 317 (0, 1, 1): 'Multithread CGI (?)', 318 (1, 0, 1): 'CGI', 319 (1, 1, 1): 'Multi thread/process CGI (?)', 320 } 321 322def handle_exception(exc_info, error_stream, html=True, 323 debug_mode=False, 324 error_email=None, 325 error_log=None, 326 show_exceptions_in_wsgi_errors=False, 327 error_email_from='errors@localhost', 328 smtp_server='localhost', 329 smtp_username=None, 330 smtp_password=None, 331 smtp_use_tls=False, 332 error_subject_prefix='', 333 error_message=None, 334 simple_html_error=False, 335 ): 336 """ 337 For exception handling outside of a web context 338 339 Use like:: 340 341 import sys 342 from paste.exceptions.errormiddleware import handle_exception 343 try: 344 do stuff 345 except: 346 handle_exception( 347 sys.exc_info(), sys.stderr, html=False, ...other config...) 348 349 If you want to report, but not fully catch the exception, call 350 ``raise`` after ``handle_exception``, which (when given no argument) 351 will reraise the exception. 352 """ 353 reported = False 354 exc_data = collector.collect_exception(*exc_info) 355 extra_data = '' 356 if error_email: 357 rep = reporter.EmailReporter( 358 to_addresses=error_email, 359 from_address=error_email_from, 360 smtp_server=smtp_server, 361 smtp_username=smtp_username, 362 smtp_password=smtp_password, 363 smtp_use_tls=smtp_use_tls, 364 subject_prefix=error_subject_prefix) 365 rep_err = send_report(rep, exc_data, html=html) 366 if rep_err: 367 extra_data += rep_err 368 else: 369 reported = True 370 if error_log: 371 rep = reporter.LogReporter( 372 filename=error_log) 373 rep_err = send_report(rep, exc_data, html=html) 374 if rep_err: 375 extra_data += rep_err 376 else: 377 reported = True 378 if show_exceptions_in_wsgi_errors: 379 rep = reporter.FileReporter( 380 file=error_stream) 381 rep_err = send_report(rep, exc_data, html=html) 382 if rep_err: 383 extra_data += rep_err 384 else: 385 reported = True 386 else: 387 line = ('Error - %s: %s\n' 388 % (exc_data.exception_type, exc_data.exception_value)) 389 if six.PY3: 390 line = line.encode('utf8') 391 error_stream.write(line) 392 if html: 393 if debug_mode and simple_html_error: 394 return_error = formatter.format_html( 395 exc_data, include_hidden_frames=False, 396 include_reusable=False, show_extra_data=False) 397 reported = True 398 elif debug_mode and not simple_html_error: 399 error_html = formatter.format_html( 400 exc_data, 401 include_hidden_frames=True, 402 include_reusable=False) 403 head_html = formatter.error_css + formatter.hide_display_js 404 return_error = error_template( 405 head_html, error_html, extra_data) 406 extra_data = '' 407 reported = True 408 else: 409 msg = error_message or ''' 410 An error occurred. See the error logs for more information. 411 (Turn debug on to display exception reports here) 412 ''' 413 return_error = error_template('', msg, '') 414 else: 415 return_error = None 416 if not reported and error_stream: 417 err_report = formatter.format_text(exc_data, show_hidden_frames=True) 418 err_report += '\n' + '-'*60 + '\n' 419 error_stream.write(err_report) 420 if extra_data: 421 error_stream.write(extra_data) 422 return return_error 423 424def send_report(rep, exc_data, html=True): 425 try: 426 rep.report(exc_data) 427 except: 428 output = StringIO() 429 traceback.print_exc(file=output) 430 if html: 431 return """ 432 <p>Additionally an error occurred while sending the %s report: 433 434 <pre>%s</pre> 435 </p>""" % ( 436 cgi.escape(str(rep)), output.getvalue()) 437 else: 438 return ( 439 "Additionally an error occurred while sending the " 440 "%s report:\n%s" % (str(rep), output.getvalue())) 441 else: 442 return '' 443 444def error_template(head_html, exception, extra): 445 return ''' 446 <html> 447 <head> 448 <title>Server Error</title> 449 %s 450 </head> 451 <body> 452 <h1>Server Error</h1> 453 %s 454 %s 455 </body> 456 </html>''' % (head_html, exception, extra) 457 458def make_error_middleware(app, global_conf, **kw): 459 return ErrorMiddleware(app, global_conf=global_conf, **kw) 460 461doc_lines = ErrorMiddleware.__doc__.splitlines(True) 462for i in range(len(doc_lines)): 463 if doc_lines[i].strip().startswith('Settings'): 464 make_error_middleware.__doc__ = ''.join(doc_lines[i:]) 465 break 466del i, doc_lines 467