diff --git a/gunicorn/util.py b/gunicorn/util.py index 11e285d5..d5730105 100644 --- a/gunicorn/util.py +++ b/gunicorn/util.py @@ -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: diff --git a/gunicorn/workers/gthread.py b/gunicorn/workers/gthread.py index 490d1ad0..cd5f86f5 100644 --- a/gunicorn/workers/gthread.py +++ b/gunicorn/workers/gthread.py @@ -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: diff --git a/gunicorn/workers/sync.py b/gunicorn/workers/sync.py index 763f8652..dc731060 100644 --- a/gunicorn/workers/sync.py +++ b/gunicorn/workers/sync.py @@ -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: