From 8e37e883eb3921d43ac9b56c56391470b6874783 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Sun, 26 Jul 2026 23:27:45 +0100 Subject: [PATCH] fix: break CurlHandle reference cycle leaking connections The header and write callbacks are non-static closures, so they bind $this. curl_setopt stores them on the CurlHandle, which the adapter already holds: adapter -> handle -> closure -> adapter. The cycle keeps the handle alive past refcount zero, so __destruct never runs until the cycle collector fires (10k roots) and every request leaks an open keep-alive connection (~1MB native TLS buffers, 2 fds). Measured in a long-running Swoole worker doing sequential requests: 150 requests = 309 open fds and 183MB private-dirty RSS; with static closures: 9 fds and flat memory. Neither callback uses $this. Co-Authored-By: Claude Fable 5 --- src/Adapter/Curl.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Adapter/Curl.php b/src/Adapter/Curl.php index 40d27ca..2e74ebd 100644 --- a/src/Adapter/Curl.php +++ b/src/Adapter/Curl.php @@ -125,7 +125,7 @@ public function send( CURLOPT_URL => $url, CURLOPT_HTTPHEADER => $formattedHeaders, CURLOPT_CUSTOMREQUEST => $method, - CURLOPT_HEADERFUNCTION => function ($curl, $header) use (&$responseHeaders) { + CURLOPT_HEADERFUNCTION => static function ($curl, $header) use (&$responseHeaders) { $len = strlen($header); $header = explode(':', $header, 2); if (count($header) < 2) { @@ -134,7 +134,7 @@ public function send( $responseHeaders[strtolower(trim($header[0]))] = trim($header[1]); return $len; }, - CURLOPT_WRITEFUNCTION => function ($ch, $data) use ($chunkCallback, &$responseBody, &$chunkIndex) { + CURLOPT_WRITEFUNCTION => static function ($ch, $data) use ($chunkCallback, &$responseBody, &$chunkIndex) { if ($chunkCallback !== null) { $chunk = new Chunk( data: $data,