Merge pull request #3590 from benoitc/fix/graceful-connection-close

fix: drain connection on close per RFC 9112 section 9.6
This commit is contained in:
Benoit Chesneau 2026-04-19 09:57:16 +02:00 committed by GitHub
commit 5de593708f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 44 additions and 16 deletions

View File

@ -279,6 +279,40 @@ def close(sock):
pass pass
def close_graceful(sock, timeout=2.0, max_drain=65536):
"""Close a TCP socket following RFC 9112 section 9.6.
Half-closes the write side to send FIN, then lingers on the read side
to drain the kernel recv buffer until the peer closes or a cap is hit,
then fully closes. This avoids the kernel sending RST (truncating the
last response segment) when unread request data remains in the buffer.
"""
try:
try:
sock.shutdown(socket.SHUT_WR)
except OSError:
return
deadline = time.monotonic() + timeout
drained = 0
while drained < max_drain:
remaining = deadline - time.monotonic()
if remaining <= 0:
break
try:
sock.settimeout(remaining)
data = sock.recv(4096)
except (socket.timeout, OSError):
break
if not data:
break
drained += len(data)
finally:
try:
sock.close()
except OSError:
pass
try: try:
from os import closerange from os import closerange
except ImportError: except ImportError:

View File

@ -15,7 +15,6 @@ import errno
import os import os
import queue import queue
import selectors import selectors
import socket
import ssl import ssl
import sys import sys
import time import time
@ -121,8 +120,12 @@ class TConn:
finally: finally:
sel.close() sel.close()
def close(self): def close(self, graceful=False):
util.close(self.sock) if graceful:
self.sock.setblocking(True)
util.close_graceful(self.sock)
else:
util.close(self.sock)
class PollableMethodQueue: class PollableMethodQueue:
@ -435,7 +438,7 @@ class ThreadWorker(base.Worker):
partial(self.on_client_socket_readable, conn)) partial(self.on_client_socket_readable, conn))
else: else:
self.nr_conns -= 1 self.nr_conns -= 1
conn.close() conn.close(graceful=True)
except Exception: except Exception:
self.nr_conns -= 1 self.nr_conns -= 1
conn.close() conn.close()
@ -693,11 +696,7 @@ class ThreadWorker(base.Worker):
# If the requests have already been sent, we should close the # If the requests have already been sent, we should close the
# connection to indicate the error. # connection to indicate the error.
self.log.exception("Error handling request") self.log.exception("Error handling request")
try: util.close_graceful(conn.sock)
conn.sock.shutdown(socket.SHUT_RDWR)
conn.sock.close()
except OSError:
pass
raise StopIteration() raise StopIteration()
raise raise
finally: finally:

View File

@ -7,7 +7,6 @@ from datetime import datetime
import errno import errno
import os import os
import select import select
import socket
import ssl import ssl
import sys import sys
@ -164,7 +163,7 @@ class SyncWorker(base.Worker):
except BaseException as e: except BaseException as e:
self.handle_error(req, client, addr, e) self.handle_error(req, client, addr, e)
finally: finally:
util.close(client) util.close_graceful(client)
def handle_request(self, listener, req, client, addr): def handle_request(self, listener, req, client, addr):
environ = {} environ = {}
@ -203,11 +202,7 @@ class SyncWorker(base.Worker):
# If the requests have already been sent, we should close the # If the requests have already been sent, we should close the
# connection to indicate the error. # connection to indicate the error.
self.log.exception("Error handling request") self.log.exception("Error handling request")
try: util.close_graceful(client)
client.shutdown(socket.SHUT_RDWR)
client.close()
except OSError:
pass
raise StopIteration() raise StopIteration()
raise raise
finally: finally: