Repository: pallets/werkzeug

Issue:
(Slightly) wrong exception raised for `abort(400)`
Calling `abort(400)` raises `ClientDisconnected`, a sub-class of `BadRequest`, instead of raising `BadRequest` as expected. Users that `try`/`except` for `BadRequest` won't notice the bug, since it is a sub-class; but if you are logging exceptions, the log lines are misleading.

Here's a trivial REPL session that demonstrates the issue:

``` python
>>> import werkzeug
>>> werkzeug.__version__
'0.9.4'
>>> from werkzeug.exceptions import abort, ClientDisconnected
>>> try:
...     abort(400)
... except ClientDisconnected:
...     print "GOT WRONG EXCEPTION"
... except Exception, e:
...     print "Got %r" % e
...
GOT WRONG EXCEPTION
>>>
```

This seems to be due to https://github.com/mitsuhiko/werkzeug/blob/master/werkzeug/exceptions.py#L542-L549 -- the sub-class will inherit the parent class's `code` attribute. So technically, this bug is non-deterministic, since it depends on the iteration order of `globals()`.



Discussion:
:+1:


Resolving patch:
diff --git a/werkzeug/exceptions.py b/werkzeug/exceptions.py
--- a/werkzeug/exceptions.py
+++ b/werkzeug/exceptions.py
@@ -542,11 +542,16 @@ class ServiceUnavailable(HTTPException):
 def _find_exceptions():
     for name, obj in iteritems(globals()):
         try:
-            if getattr(obj, 'code', None) is not None:
-                default_exceptions[obj.code] = obj
-                __all__.append(obj.__name__)
-        except TypeError: # pragma: no cover
+            is_http_exception = issubclass(obj, HTTPException)
+        except TypeError:
+            is_http_exception = False
+        if not is_http_exception or obj.code is None:
             continue
+        __all__.append(obj.__name__)
+        old_obj = default_exceptions.get(obj.code, None)
+        if old_obj is not None and issubclass(obj, old_obj):
+            continue
+        default_exceptions[obj.code] = obj
 _find_exceptions()
 del _find_exceptions
 
diff --git a/werkzeug/testsuite/exceptions.py b/werkzeug/testsuite/exceptions.py
--- a/werkzeug/testsuite/exceptions.py
+++ b/werkzeug/testsuite/exceptions.py
@@ -36,24 +36,34 @@ def test_proxy_exception(self):
 
     def test_aborter(self):
         abort = exceptions.abort
-        self.assert_raises(exceptions.BadRequest, abort, 400)
-        self.assert_raises(exceptions.Unauthorized, abort, 401)
-        self.assert_raises(exceptions.Forbidden, abort, 403)
-        self.assert_raises(exceptions.NotFound, abort, 404)
-        self.assert_raises(exceptions.MethodNotAllowed, abort, 405, ['GET', 'HEAD'])
-        self.assert_raises(exceptions.NotAcceptable, abort, 406)
-        self.assert_raises(exceptions.RequestTimeout, abort, 408)
-        self.assert_raises(exceptions.Gone, abort, 410)
-        self.assert_raises(exceptions.LengthRequired, abort, 411)
-        self.assert_raises(exceptions.PreconditionFailed, abort, 412)
-        self.assert_raises(exceptions.RequestEntityTooLarge, abort, 413)
-        self.assert_raises(exceptions.RequestURITooLarge, abort, 414)
-        self.assert_raises(exceptions.UnsupportedMediaType, abort, 415)
-        self.assert_raises(exceptions.UnprocessableEntity, abort, 422)
-        self.assert_raises(exceptions.InternalServerError, abort, 500)
-        self.assert_raises(exceptions.NotImplemented, abort, 501)
-        self.assert_raises(exceptions.BadGateway, abort, 502)
-        self.assert_raises(exceptions.ServiceUnavailable, abort, 503)
+
+        for test in [
+            (exceptions.BadRequest, abort, 400),
+            (exceptions.Unauthorized, abort, 401),
+            (exceptions.Forbidden, abort, 403),
+            (exceptions.NotFound, abort, 404),
+            (exceptions.MethodNotAllowed, abort, 405, ['GET', 'HEAD']),
+            (exceptions.NotAcceptable, abort, 406),
+            (exceptions.RequestTimeout, abort, 408),
+            (exceptions.Gone, abort, 410),
+            (exceptions.LengthRequired, abort, 411),
+            (exceptions.PreconditionFailed, abort, 412),
+            (exceptions.RequestEntityTooLarge, abort, 413),
+            (exceptions.RequestURITooLarge, abort, 414),
+            (exceptions.UnsupportedMediaType, abort, 415),
+            (exceptions.UnprocessableEntity, abort, 422),
+            (exceptions.InternalServerError, abort, 500),
+            (exceptions.NotImplemented, abort, 501),
+            (exceptions.BadGateway, abort, 502),
+            (exceptions.ServiceUnavailable, abort, 503)
+        ]:
+            exc_type = test[0]
+            func = test[1]
+            args = test[2:]
+            with self.assert_raises(exc_type) as exc_info:
+                func(*args)
+
+            self.assert_is(type(exc_info.exc_value), exc_type)
 
         myabort = exceptions.Aborter({1: exceptions.NotFound})
         self.assert_raises(LookupError, myabort, 404)


Test patch: