fix: drain connection on close per RFC 9112 section 9.6

Avoids TCP RST truncating the response tail when unread request data
(body, pipelined bytes, trailers) sits in the kernel recv buffer at
close time. Half-closes write, linger-reads (bounded 2s / 64 KB),
then closes.
This commit is contained in:
Benoit Chesneau 2026-04-19 09:39:27 +02:00
parent 9aa54703f4
commit 9d422c3ef0
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: