From 732932994bb7e2f3ff1d3d6358362a71525a90e6 Mon Sep 17 00:00:00 2001 From: lazerg Date: Tue, 7 Jul 2026 23:15:49 +0500 Subject: [PATCH] bugfix: skip error parsing for 2xx responses on rejected promises --- ...ix-wrappedhttphandler-2xx-error-parse.json | 7 ++++++ src/WrappedHttpHandler.php | 10 ++++++-- tests/WrappedHttpHandlerTest.php | 23 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 .changes/nextrelease/fix-wrappedhttphandler-2xx-error-parse.json diff --git a/.changes/nextrelease/fix-wrappedhttphandler-2xx-error-parse.json b/.changes/nextrelease/fix-wrappedhttphandler-2xx-error-parse.json new file mode 100644 index 0000000000..864a22797b --- /dev/null +++ b/.changes/nextrelease/fix-wrappedhttphandler-2xx-error-parse.json @@ -0,0 +1,7 @@ +[ + { + "type": "bugfix", + "category": "", + "description": "Fixed WrappedHttpHandler buffering the entire partial response body into memory when a 2xx response was attached to a rejected promise, which could exhaust memory on a transport failure during a large streamed download." + } +] diff --git a/src/WrappedHttpHandler.php b/src/WrappedHttpHandler.php index c6cfa3260c..87cd19d46b 100644 --- a/src/WrappedHttpHandler.php +++ b/src/WrappedHttpHandler.php @@ -168,8 +168,14 @@ private function parseError( $serviceError = "AWS HTTP error:\n"; - if (!isset($err['response'])) { - $parts = ['response' => null]; + // A 2xx status on a rejected promise means the transport failed + // mid-body after a success response was received; there is no error + // document to parse, and parsing one would buffer the entire partial + // body into memory. + if (!isset($err['response']) + || $err['response']->getStatusCode() < 300 + ) { + $parts = ['response' => $err['response'] ?? null]; $serviceError .= $err['exception']->getMessage(); } else { try { diff --git a/tests/WrappedHttpHandlerTest.php b/tests/WrappedHttpHandlerTest.php index 0f4ffae490..e0de762e37 100644 --- a/tests/WrappedHttpHandlerTest.php +++ b/tests/WrappedHttpHandlerTest.php @@ -93,6 +93,29 @@ public function testCanRejectWithoutResponse() } } + public function testDoesNotParseErrorWhenRejectedWithSuccessResponse() + { + $e = new \Exception('cURL error 18: transfer closed with outstanding read data remaining'); + $cmd = new Command('foo'); + $req = new Request('GET', 'http://foo.com'); + $res = new Response(200, [], 'partial body'); + $handler = function () use ($e, $res) { + return new RejectedPromise(['exception' => $e, 'response' => $res]); + }; + $parser = $errorParser = [$this, 'fail']; + $wrapped = new WrappedHttpHandler($handler, $parser, $errorParser); + try { + $wrapped($cmd, $req)->wait(); + $this->fail(); + } catch (AwsException $e) { + $this->assertSame($cmd, $e->getCommand()); + $this->assertSame($req, $e->getRequest()); + $this->assertSame($res, $e->getResponse()); + $this->assertNull($e->getResult()); + $this->assertStringContainsString('cURL error 18', $e->getMessage()); + } + } + #[DataProvider('responseAndParserProvider')] public function testCanRejectWithAndParseResponse( Response $res,