Repository: pallets/werkzeug

Issue:
When WSGIRequestHandler catches socket.error it silences all other OSErrors in Python 3
Reported at pallets/flask#1995. `WSGIRequestHander.run_wsgi` catches `socket.error` to silence client disconnects. In Python 2 this was fine, because `socket.error` was unique. In Python 3, `socket.error is OSError`, so any `OSError` or subclass (`IOError`, etc.) is also silenced.

Possible solution is to check if `e.errno` is a disconnect-related error and bubble the error up otherwise. Or catch a more specific error such as [`ConnectionError`](https://docs.python.org/3/library/exceptions.html#ConnectionError) in Python 3.

This only affects Werkzeug's dev server, other WSGI servers like Gunicorn do not exhibit this behavior.


Discussion:
I feel like we had this before. I thought we fixed this somewhere already.
It was reported against Flask and we discussed it there, but it's a bug against Werkzeug so I moved it here. Maybe that's what you're remembering? pallets/flask#1995
Can I work on this? If so, any suggestion/advice to begin with that consideration?
@aaossa I'm not sure what you mean with "that consideration". One possible approach to this issue is described in the OP
With "that consideration" I meant the suggestion/advice I was asking for. I'll give it a try.
I have a possible solution [here](https://github.com/aaossa/werkzeug/commit/35cecf481c1595add85d33829ea1f6e6538aeca0), should I create a test? @untitaker 
I haven't looked at this in a while, but `type(e) is socket.error` is the same issue we're talking about here: it's not a distinct error on py3. This looks like it just moved the check around, it didn't change the logic.
I though that the problem was using `except (socket.error ...` because it uses `isinstance` instead of `type` and that caused the problems in error handling, because `isinstance` check subclasses and `type` does not.

**EDIT:** I tried the example from the original issue and worked without problems, but I haven't tested with a "pure" `socket.error`.
`isinstance` is the same as `type is` in this case, because `socket.error = OSError`.
Then maybe use something like:

```python
# (Not valid syntax)
except socket.timeout, _:
    # ...
except socket.error, exc:
    print("Caught exception socket.error : " + str(exc))
```

and check the error code (using [`errno`](https://docs.python.org/3.6/library/errno.html)) like [this](https://stackoverflow.com/a/5161286/3281097) should work? I'm trying to find the errno for `socket.error`
That's what I said at the top. Also, you're using really old syntax for catching errors there.

Resolving patch:
diff --git a/werkzeug/serving.py b/werkzeug/serving.py
--- a/werkzeug/serving.py
+++ b/werkzeug/serving.py
@@ -103,6 +103,14 @@ class ForkingMixIn(object):
 LISTEN_QUEUE = 128
 can_open_by_fd = not WIN and hasattr(socket, 'fromfd')
 
+# On Python 3, ConnectionError represents the same errnos as
+# socket.error from Python 2, while socket.error is an alias for the
+# more generic OSError.
+if PY2:
+    _ConnectionError = socket.error
+else:
+    _ConnectionError = ConnectionError
+
 
 class DechunkedInput(io.RawIOBase):
     """An input stream that handles Transfer-Encoding 'chunked'"""
@@ -282,7 +290,7 @@ def execute(app):
 
         try:
             execute(self.server.app)
-        except (socket.error, socket.timeout) as e:
+        except (_ConnectionError, socket.timeout) as e:
             self.connection_dropped(e, environ)
         except Exception:
             if self.server.passthrough_errors:
@@ -305,10 +313,10 @@ def handle(self):
         rv = None
         try:
             rv = BaseHTTPRequestHandler.handle(self)
-        except (socket.error, socket.timeout) as e:
+        except (_ConnectionError, socket.timeout) as e:
             self.connection_dropped(e)
-        except Exception:
-            if self.server.ssl_context is None or not is_ssl_error():
+        except Exception as e:
+            if self.server.ssl_context is None or not is_ssl_error(e):
                 raise
         if self.server.shutdown_signal:
             self.initiate_shutdown()
@@ -681,6 +689,10 @@ def serve_forever(self):
     def handle_error(self, request, client_address):
         if self.passthrough_errors:
             raise
+        # Python 2 still causes a socket.error after the earlier
+        # handling, so silence it here.
+        if isinstance(sys.exc_info()[1], _ConnectionError):
+            return
         return HTTPServer.handle_error(self, request, client_address)
 
     def get_request(self):


Test patch: