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
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:
from os import closerange
except ImportError:

View File

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

View File

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