Skip to content

Commit aed464b

Browse files
committed
Merge pull request pallets#1291 from flying-sheep/errorhandler-rework
Fixed and intuitivized exception handling
2 parents 06f2be3 + b31252d commit aed464b

File tree

2 files changed

+182
-30
lines changed

2 files changed

+182
-30
lines changed

flask/app.py

Lines changed: 69 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,18 @@
88
:copyright: (c) 2015 by Armin Ronacher.
99
:license: BSD, see LICENSE for more details.
1010
"""
11-
1211
import os
1312
import sys
1413
from threading import Lock
1514
from datetime import timedelta
1615
from itertools import chain
1716
from functools import update_wrapper
17+
from collections import Mapping
1818

1919
from werkzeug.datastructures import ImmutableDict
2020
from werkzeug.routing import Map, Rule, RequestRedirect, BuildError
2121
from werkzeug.exceptions import HTTPException, InternalServerError, \
22-
MethodNotAllowed, BadRequest
22+
MethodNotAllowed, BadRequest, default_exceptions
2323

2424
from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \
2525
locked_cached_property, _endpoint_from_view_func, find_package
@@ -33,7 +33,7 @@
3333
_default_template_ctx_processor
3434
from .signals import request_started, request_finished, got_request_exception, \
3535
request_tearing_down, appcontext_tearing_down
36-
from ._compat import reraise, string_types, text_type, integer_types
36+
from ._compat import reraise, string_types, text_type, integer_types, iterkeys
3737

3838
# a lock used for logger initialization
3939
_logger_lock = Lock()
@@ -1078,6 +1078,21 @@ def decorator(f):
10781078
return f
10791079
return decorator
10801080

1081+
@staticmethod
1082+
def _get_exc_class_and_code(exc_class_or_code):
1083+
"""Ensure that we register only exceptions as handler keys"""
1084+
if isinstance(exc_class_or_code, integer_types):
1085+
exc_class = default_exceptions[exc_class_or_code]
1086+
else:
1087+
exc_class = exc_class_or_code
1088+
1089+
assert issubclass(exc_class, Exception)
1090+
1091+
if issubclass(exc_class, HTTPException):
1092+
return exc_class, exc_class.code
1093+
else:
1094+
return exc_class, None
1095+
10811096
@setupmethod
10821097
def errorhandler(self, code_or_exception):
10831098
"""A decorator that is used to register a function give a given
@@ -1136,16 +1151,21 @@ def register_error_handler(self, code_or_exception, f):
11361151

11371152
@setupmethod
11381153
def _register_error_handler(self, key, code_or_exception, f):
1139-
if isinstance(code_or_exception, HTTPException):
1140-
code_or_exception = code_or_exception.code
1141-
if isinstance(code_or_exception, integer_types):
1142-
assert code_or_exception != 500 or key is None, \
1143-
'It is currently not possible to register a 500 internal ' \
1144-
'server error on a per-blueprint level.'
1145-
self.error_handler_spec.setdefault(key, {})[code_or_exception] = f
1146-
else:
1147-
self.error_handler_spec.setdefault(key, {}).setdefault(None, []) \
1148-
.append((code_or_exception, f))
1154+
"""
1155+
:type key: None|str
1156+
:type code_or_exception: int|T<=Exception
1157+
:type f: callable
1158+
"""
1159+
if isinstance(code_or_exception, HTTPException): # old broken behavior
1160+
raise ValueError(
1161+
'Tried to register a handler for an exception instance {0!r}. '
1162+
'Handlers can only be registered for exception classes or HTTP error codes.'
1163+
.format(code_or_exception))
1164+
1165+
exc_class, code = self._get_exc_class_and_code(code_or_exception)
1166+
1167+
handlers = self.error_handler_spec.setdefault(key, {}).setdefault(code, {})
1168+
handlers[exc_class] = f
11491169

11501170
@setupmethod
11511171
def template_filter(self, name=None):
@@ -1386,22 +1406,46 @@ def url_defaults(self, f):
13861406
self.url_default_functions.setdefault(None, []).append(f)
13871407
return f
13881408

1409+
def _find_error_handler(self, e):
1410+
"""Finds a registered error handler for the request’s blueprint.
1411+
If neither blueprint nor App has a suitable handler registered, returns None
1412+
"""
1413+
exc_class, code = self._get_exc_class_and_code(type(e))
1414+
1415+
def find_superclass(handler_map):
1416+
if not handler_map:
1417+
return None
1418+
for superclass in exc_class.__mro__:
1419+
if superclass is BaseException:
1420+
return None
1421+
handler = handler_map.get(superclass)
1422+
if handler is not None:
1423+
handler_map[exc_class] = handler # cache for next time exc_class is raised
1424+
return handler
1425+
return None
1426+
1427+
# try blueprint handlers
1428+
handler = find_superclass(self.error_handler_spec.get(request.blueprint, {}).get(code))
1429+
1430+
if handler is not None:
1431+
return handler
1432+
1433+
# fall back to app handlers
1434+
return find_superclass(self.error_handler_spec[None].get(code))
1435+
13891436
def handle_http_exception(self, e):
13901437
"""Handles an HTTP exception. By default this will invoke the
13911438
registered error handlers and fall back to returning the
13921439
exception as response.
13931440
13941441
.. versionadded:: 0.3
13951442
"""
1396-
handlers = self.error_handler_spec.get(request.blueprint)
13971443
# Proxy exceptions don't have error codes. We want to always return
13981444
# those unchanged as errors
13991445
if e.code is None:
14001446
return e
1401-
if handlers and e.code in handlers:
1402-
handler = handlers[e.code]
1403-
else:
1404-
handler = self.error_handler_spec[None].get(e.code)
1447+
1448+
handler = self._find_error_handler(e)
14051449
if handler is None:
14061450
return e
14071451
return handler(e)
@@ -1443,20 +1487,15 @@ def handle_user_exception(self, e):
14431487
# wants the traceback preserved in handle_http_exception. Of course
14441488
# we cannot prevent users from trashing it themselves in a custom
14451489
# trap_http_exception method so that's their fault then.
1446-
1447-
blueprint_handlers = ()
1448-
handlers = self.error_handler_spec.get(request.blueprint)
1449-
if handlers is not None:
1450-
blueprint_handlers = handlers.get(None, ())
1451-
app_handlers = self.error_handler_spec[None].get(None, ())
1452-
for typecheck, handler in chain(blueprint_handlers, app_handlers):
1453-
if isinstance(e, typecheck):
1454-
return handler(e)
1455-
1490+
14561491
if isinstance(e, HTTPException) and not self.trap_http_exception(e):
14571492
return self.handle_http_exception(e)
14581493

1459-
reraise(exc_type, exc_value, tb)
1494+
handler = self._find_error_handler(e)
1495+
1496+
if handler is None:
1497+
reraise(exc_type, exc_value, tb)
1498+
return handler(e)
14601499

14611500
def handle_exception(self, e):
14621501
"""Default exception handling that kicks in when an exception
@@ -1470,7 +1509,7 @@ def handle_exception(self, e):
14701509
exc_type, exc_value, tb = sys.exc_info()
14711510

14721511
got_request_exception.send(self, exception=e)
1473-
handler = self.error_handler_spec[None].get(500)
1512+
handler = self._find_error_handler(InternalServerError())
14741513

14751514
if self.propagate_exceptions:
14761515
# if we want to repropagate the exception, we can attempt to

tests/test_user_error_handler.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# -*- coding: utf-8 -*-
2+
from werkzeug.exceptions import Forbidden, InternalServerError
3+
import flask
4+
5+
6+
def test_error_handler_subclass():
7+
app = flask.Flask(__name__)
8+
9+
class ParentException(Exception):
10+
pass
11+
12+
class ChildExceptionUnregistered(ParentException):
13+
pass
14+
15+
class ChildExceptionRegistered(ParentException):
16+
pass
17+
18+
@app.errorhandler(ParentException)
19+
def parent_exception_handler(e):
20+
assert isinstance(e, ParentException)
21+
return 'parent'
22+
23+
@app.errorhandler(ChildExceptionRegistered)
24+
def child_exception_handler(e):
25+
assert isinstance(e, ChildExceptionRegistered)
26+
return 'child-registered'
27+
28+
@app.route('/parent')
29+
def parent_test():
30+
raise ParentException()
31+
32+
@app.route('/child-unregistered')
33+
def unregistered_test():
34+
raise ChildExceptionUnregistered()
35+
36+
@app.route('/child-registered')
37+
def registered_test():
38+
raise ChildExceptionRegistered()
39+
40+
41+
c = app.test_client()
42+
43+
assert c.get('/parent').data == b'parent'
44+
assert c.get('/child-unregistered').data == b'parent'
45+
assert c.get('/child-registered').data == b'child-registered'
46+
47+
48+
def test_error_handler_http_subclass():
49+
app = flask.Flask(__name__)
50+
51+
class ForbiddenSubclassRegistered(Forbidden):
52+
pass
53+
54+
class ForbiddenSubclassUnregistered(Forbidden):
55+
pass
56+
57+
@app.errorhandler(403)
58+
def code_exception_handler(e):
59+
assert isinstance(e, Forbidden)
60+
return 'forbidden'
61+
62+
@app.errorhandler(ForbiddenSubclassRegistered)
63+
def subclass_exception_handler(e):
64+
assert isinstance(e, ForbiddenSubclassRegistered)
65+
return 'forbidden-registered'
66+
67+
@app.route('/forbidden')
68+
def forbidden_test():
69+
raise Forbidden()
70+
71+
@app.route('/forbidden-registered')
72+
def registered_test():
73+
raise ForbiddenSubclassRegistered()
74+
75+
@app.route('/forbidden-unregistered')
76+
def unregistered_test():
77+
raise ForbiddenSubclassUnregistered()
78+
79+
80+
c = app.test_client()
81+
82+
assert c.get('/forbidden').data == b'forbidden'
83+
assert c.get('/forbidden-unregistered').data == b'forbidden'
84+
assert c.get('/forbidden-registered').data == b'forbidden-registered'
85+
86+
87+
def test_error_handler_blueprint():
88+
bp = flask.Blueprint('bp', __name__)
89+
90+
@bp.errorhandler(500)
91+
def bp_exception_handler(e):
92+
return 'bp-error'
93+
94+
@bp.route('/error')
95+
def bp_test():
96+
raise InternalServerError()
97+
98+
app = flask.Flask(__name__)
99+
100+
@app.errorhandler(500)
101+
def app_exception_handler(e):
102+
return 'app-error'
103+
104+
@app.route('/error')
105+
def app_test():
106+
raise InternalServerError()
107+
108+
app.register_blueprint(bp, url_prefix='/bp')
109+
110+
c = app.test_client()
111+
112+
assert c.get('/error').data == b'app-error'
113+
assert c.get('/bp/error').data == b'bp-error'

0 commit comments

Comments
 (0)