Skip to content

Commit

Permalink
🐛 Suppress legit OS errors on socket shutdown
Browse files Browse the repository at this point in the history
When closing a socket we normally want to terminate the transport-
level connection by sending a TCP FIN packet over the wire. This works
great if the client is willing to perform a 3-way handshake but
sometimes the client-side just drops the connection by sending an RST
instead of a FIN. In this case, our server-side `socket.shutdown()`
call will raise an OSError (socket.error on Python 2) or its
derivatives. Which is perfectly fine and its alright for us to ignore
those.

The kernel socket close helper rewrite introduced a behavior of only
suppessing ENOTCONN in v8.4.8 (the case when the client is no longer
with us) but it left own a few other cases that may be happening too.

This change fixes that by extending the list of errors that are to be
suppressed. Here's what's handled from now on:
  * ENOTCONN — client is no longer connected
  * EPIPE — write on a pipe while the other end has been closed
  * ESHUTDOWN — write on a socket which has been shutdown for writing
  * ECONNRESET — connection is reset by the peer

Fixes #341

Refs:
  * https://en.wikipedia.org/wiki/Transmission_Control_Protocol#Connection_termination
  * #341 (comment)
  • Loading branch information
webknjaz committed Nov 30, 2020
1 parent 4c71c6f commit 3095268
Show file tree
Hide file tree
Showing 2 changed files with 24 additions and 3 deletions.
21 changes: 21 additions & 0 deletions cheroot/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,24 @@ def plat_specific_errors(*errnames):
if sys.platform == 'darwin':
socket_errors_to_ignore.extend(plat_specific_errors('EPROTOTYPE'))
socket_errors_nonblocking.extend(plat_specific_errors('EPROTOTYPE'))


acceptable_sock_shutdown_error_codes = {
errno.ENOTCONN,
errno.EPIPE, errno.ESHUTDOWN, # corresponds to BrokenPipeError in Python 3
errno.ECONNRESET, # corresponds to ConnectionResetError in Python 3
}
"""Errors that may happen during the connection close sequence.
* ENOTCONN — client is no longer connected
* EPIPE — write on a pipe while the other end has been closed
* ESHUTDOWN — write on a socket which has been shutdown for writing
* ECONNRESET — connection is reset by the peer
Ref: https://github.com/cherrypy/cheroot/issues/341#issuecomment-735884889
"""

try: # py3
broken_connection_exceptions = (BrokenPipeError, ConnectionResetError)
except NameError: # py2
broken_connection_exceptions = ()
6 changes: 3 additions & 3 deletions cheroot/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@
import platform
import contextlib
import threading
import errno

try:
from functools import lru_cache
Expand Down Expand Up @@ -1478,9 +1477,10 @@ def _close_kernel_socket(self):

try:
shutdown(socket.SHUT_RDWR) # actually send a TCP FIN
except errors.broken_connection_exceptions:
pass
except socket.error as e:
# Suppress "client is no longer connected"
if e.errno != errno.ENOTCONN:
if e.errno not in errors.acceptable_sock_shutdown_error_codes:
raise


Expand Down

0 comments on commit 3095268

Please sign in to comment.