Repository: php/pie (php)

Issue:
Switch from guzzle / psr7 to composer's HttpDownloader
Composer already has a battle-tested HTTP client built in.
What about leveraging it? That'd make the experience more consistent with composer itself.

Pull request:
Use Composer HttpDownloader and remove Guzzle
Fixes #50 

Patch:
diff --git a/composer.json b/composer.json
index 25f4822..dfc7b70 100644
--- a/composer.json
+++ b/composer.json
@@ -30,8 +30,6 @@
         "php": "8.1.*||8.2.*||8.3.*||8.4.*",
         "ext-zip": "*",
         "composer/composer": "^2.8.2",
-        "guzzlehttp/guzzle": "^7.8",
-        "guzzlehttp/psr7": "^2.6",
         "illuminate/container": "^10.47",
         "psr/http-message": "^2.0",
         "symfony/console": "^6.4",
diff --git a/composer.lock b/composer.lock
index 363d4fa..6f37b0e 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "fc8374dc9a7a4fb1f2a230d5a2cbb622",
+    "content-hash": "70b72907e89a5c18540dfa56ea943fb6",
     "packages": [
         {
             "name": "composer/ca-bundle",
@@ -644,331 +644,6 @@
             ],
             "time": "2024-05-06T16:37:16+00:00"
         },
-        {
-            "name": "guzzlehttp/guzzle",
-            "version": "7.9.2",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/guzzle/guzzle.git",
-                "reference": "d281ed313b989f213357e3be1a179f02196ac99b"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b",
-                "reference": "d281ed313b989f213357e3be1a179f02196ac99b",
-                "shasum": ""
-            },
-            "require": {
-                "ext-json": "*",
-                "guzzlehttp/promises": "^1.5.3 || ^2.0.3",
-                "guzzlehttp/psr7": "^2.7.0",
-                "php": "^7.2.5 || ^8.0",
-                "psr/http-client": "^1.0",
-                "symfony/deprecation-contracts": "^2.2 || ^3.0"
-            },
-            "provide": {
-                "psr/http-client-implementation": "1.0"
-            },
-            "require-dev": {
-                "bamarni/composer-bin-plugin": "^1.8.2",
-                "ext-curl": "*",
-                "guzzle/client-integration-tests": "3.0.2",
-                "php-http/message-factory": "^1.1",
-                "phpunit/phpunit": "^8.5.39 || ^9.6.20",
-                "psr/log": "^1.1 || ^2.0 || ^3.0"
-            },
-            "suggest": {
-                "ext-curl": "Required for CURL handler support",
-                "ext-intl": "Required for Internationalized Domain Name (IDN) support",
-                "psr/log": "Required for using the Log middleware"
-            },
-            "type": "library",
-            "extra": {
-                "bamarni-bin": {
-                    "bin-links": true,
-                    "forward-command": false
-                }
-            },
-            "autoload": {
-                "files": [
-                    "src/functions_include.php"
-                ],
-                "psr-4": {
-                    "GuzzleHttp\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Graham Campbell",
-                    "email": "hello@gjcampbell.co.uk",
-                    "homepage": "https://github.com/GrahamCampbell"
-                },
-                {
-                    "name": "Michael Dowling",
-                    "email": "mtdowling@gmail.com",
-                    "homepage": "https://github.com/mtdowling"
-                },
-                {
-                    "name": "Jeremy Lindblom",
-                    "email": "jeremeamia@gmail.com",
-                    "homepage": "https://github.com/jeremeamia"
-                },
-                {
-                    "name": "George Mponos",
-                    "email": "gmponos@gmail.com",
-                    "homepage": "https://github.com/gmponos"
-                },
-                {
-                    "name": "Tobias Nyholm",
-                    "email": "tobias.nyholm@gmail.com",
-                    "homepage": "https://github.com/Nyholm"
-                },
-                {
-                    "name": "Márk Sági-Kazár",
-                    "email": "mark.sagikazar@gmail.com",
-                    "homepage": "https://github.com/sagikazarmark"
-                },
-                {
-                    "name": "Tobias Schultze",
-                    "email": "webmaster@tubo-world.de",
-                    "homepage": "https://github.com/Tobion"
-                }
-            ],
-            "description": "Guzzle is a PHP HTTP client library",
-            "keywords": [
-                "client",
-                "curl",
-                "framework",
-                "http",
-                "http client",
-                "psr-18",
-                "psr-7",
-                "rest",
-                "web service"
-            ],
-            "support": {
-                "issues": "https://github.com/guzzle/guzzle/issues",
-                "source": "https://github.com/guzzle/guzzle/tree/7.9.2"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/GrahamCampbell",
-                    "type": "github"
-                },
-                {
-                    "url": "https://github.com/Nyholm",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2024-07-24T11:22:20+00:00"
-        },
-        {
-            "name": "guzzlehttp/promises",
-            "version": "2.0.4",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/guzzle/promises.git",
-                "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455",
-                "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^7.2.5 || ^8.0"
-            },
-            "require-dev": {
-                "bamarni/composer-bin-plugin": "^1.8.2",
-                "phpunit/phpunit": "^8.5.39 || ^9.6.20"
-            },
-            "type": "library",
-            "extra": {
-                "bamarni-bin": {
-                    "bin-links": true,
-                    "forward-command": false
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "GuzzleHttp\\Promise\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Graham Campbell",
-                    "email": "hello@gjcampbell.co.uk",
-                    "homepage": "https://github.com/GrahamCampbell"
-                },
-                {
-                    "name": "Michael Dowling",
-                    "email": "mtdowling@gmail.com",
-                    "homepage": "https://github.com/mtdowling"
-                },
-                {
-                    "name": "Tobias Nyholm",
-                    "email": "tobias.nyholm@gmail.com",
-                    "homepage": "https://github.com/Nyholm"
-                },
-                {
-                    "name": "Tobias Schultze",
-                    "email": "webmaster@tubo-world.de",
-                    "homepage": "https://github.com/Tobion"
-                }
-            ],
-            "description": "Guzzle promises library",
-            "keywords": [
-                "promise"
-            ],
-            "support": {
-                "issues": "https://github.com/guzzle/promises/issues",
-                "source": "https://github.com/guzzle/promises/tree/2.0.4"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/GrahamCampbell",
-                    "type": "github"
-                },
-                {
-                    "url": "https://github.com/Nyholm",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2024-10-17T10:06:22+00:00"
-        },
-        {
-            "name": "guzzlehttp/psr7",
-            "version": "2.7.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/guzzle/psr7.git",
-                "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201",
-                "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^7.2.5 || ^8.0",
-                "psr/http-factory": "^1.0",
-                "psr/http-message": "^1.1 || ^2.0",
-                "ralouphie/getallheaders": "^3.0"
-            },
-            "provide": {
-                "psr/http-factory-implementation": "1.0",
-                "psr/http-message-implementation": "1.0"
-            },
-            "require-dev": {
-                "bamarni/composer-bin-plugin": "^1.8.2",
-                "http-interop/http-factory-tests": "0.9.0",
-                "phpunit/phpunit": "^8.5.39 || ^9.6.20"
-            },
-            "suggest": {
-                "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
-            },
-            "type": "library",
-            "extra": {
-                "bamarni-bin": {
-                    "bin-links": true,
-                    "forward-command": false
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "GuzzleHttp\\Psr7\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Graham Campbell",
-                    "email": "hello@gjcampbell.co.uk",
-                    "homepage": "https://github.com/GrahamCampbell"
-                },
-                {
-                    "name": "Michael Dowling",
-                    "email": "mtdowling@gmail.com",
-                    "homepage": "https://github.com/mtdowling"
-                },
-                {
-                    "name": "George Mponos",
-                    "email": "gmponos@gmail.com",
-                    "homepage": "https://github.com/gmponos"
-                },
-                {
-                    "name": "Tobias Nyholm",
-                    "email": "tobias.nyholm@gmail.com",
-                    "homepage": "https://github.com/Nyholm"
-                },
-                {
-                    "name": "Márk Sági-Kazár",
-                    "email": "mark.sagikazar@gmail.com",
-                    "homepage": "https://github.com/sagikazarmark"
-                },
-                {
-                    "name": "Tobias Schultze",
-                    "email": "webmaster@tubo-world.de",
-                    "homepage": "https://github.com/Tobion"
-                },
-                {
-                    "name": "Márk Sági-Kazár",
-                    "email": "mark.sagikazar@gmail.com",
-                    "homepage": "https://sagikazarmark.hu"
-                }
-            ],
-            "description": "PSR-7 message implementation that also provides common utility methods",
-            "keywords": [
-                "http",
-                "message",
-                "psr-7",
-                "request",
-                "response",
-                "stream",
-                "uri",
-                "url"
-            ],
-            "support": {
-                "issues": "https://github.com/guzzle/psr7/issues",
-                "source": "https://github.com/guzzle/psr7/tree/2.7.0"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/GrahamCampbell",
-                    "type": "github"
-                },
-                {
-                    "url": "https://github.com/Nyholm",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2024-07-18T11:15:46+00:00"
-        },
         {
             "name": "illuminate/container",
             "version": "v10.48.23",
@@ -1186,113 +861,6 @@
             },
             "time": "2021-11-05T16:47:00+00:00"
         },
-        {
-            "name": "psr/http-client",
-            "version": "1.0.3",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/php-fig/http-client.git",
-                "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
-                "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^7.0 || ^8.0",
-                "psr/http-message": "^1.0 || ^2.0"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.0.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Psr\\Http\\Client\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "PHP-FIG",
-                    "homepage": "https://www.php-fig.org/"
-                }
-            ],
-            "description": "Common interface for HTTP clients",
-            "homepage": "https://github.com/php-fig/http-client",
-            "keywords": [
-                "http",
-                "http-client",
-                "psr",
-                "psr-18"
-            ],
-            "support": {
-                "source": "https://github.com/php-fig/http-client"
-            },
-            "time": "2023-09-23T14:17:50+00:00"
-        },
-        {
-            "name": "psr/http-factory",
-            "version": "1.1.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/php-fig/http-factory.git",
-                "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
-                "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1",
-                "psr/http-message": "^1.0 || ^2.0"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.0.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Psr\\Http\\Message\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "PHP-FIG",
-                    "homepage": "https://www.php-fig.org/"
-                }
-            ],
-            "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
-            "keywords": [
-                "factory",
-                "http",
-                "message",
-                "psr",
-                "psr-17",
-                "psr-7",
-                "request",
-                "response"
-            ],
-            "support": {
-                "source": "https://github.com/php-fig/http-factory"
-            },
-            "time": "2024-04-15T12:06:14+00:00"
-        },
         {
             "name": "psr/http-message",
             "version": "2.0",
@@ -1447,50 +1015,6 @@
             },
             "time": "2021-10-29T13:26:27+00:00"
         },
-        {
-            "name": "ralouphie/getallheaders",
-            "version": "3.0.3",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/ralouphie/getallheaders.git",
-                "reference": "120b605dfeb996808c31b6477290a714d356e822"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
-                "reference": "120b605dfeb996808c31b6477290a714d356e822",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.6"
-            },
-            "require-dev": {
-                "php-coveralls/php-coveralls": "^2.1",
-                "phpunit/phpunit": "^5 || ^6.5"
-            },
-            "type": "library",
-            "autoload": {
-                "files": [
-                    "src/getallheaders.php"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Ralph Khattar",
-                    "email": "ralph.khattar@gmail.com"
-                }
-            ],
-            "description": "A polyfill for getallheaders.",
-            "support": {
-                "issues": "https://github.com/ralouphie/getallheaders/issues",
-                "source": "https://github.com/ralouphie/getallheaders/tree/develop"
-            },
-            "time": "2019-03-08T08:55:37+00:00"
-        },
         {
             "name": "react/promise",
             "version": "v3.2.0",
diff --git a/src/ComposerIntegration/OverrideWindowsUrlInstallListener.php b/src/ComposerIntegration/OverrideWindowsUrlInstallListener.php
index 5c3213f..43e064a 100644
--- a/src/ComposerIntegration/OverrideWindowsUrlInstallListener.php
+++ b/src/ComposerIntegration/OverrideWindowsUrlInstallListener.php
@@ -11,6 +11,7 @@ use Composer\Installer\InstallerEvents;
 use Composer\IO\IOInterface;
 use Composer\Package\CompletePackageInterface;
 use Composer\Util\AuthHelper;
+use Composer\Util\HttpDownloader;
 use Php\Pie\DependencyResolver\Package;
 use Php\Pie\Downloading\PackageReleaseAssets;
 use Php\Pie\Platform\OperatingSystem;
@@ -65,6 +66,7 @@ class OverrideWindowsUrlInstallListener
             $this->composerRequest->targetPlatform,
             Package::fromComposerCompletePackage($composerPackage),
             new AuthHelper($this->io, $this->composer->getConfig()),
+            new HttpDownloader($this->io, $this->composer->getConfig()),
         );
 
         $this->composerRequest->pieOutput->writeln('Found prebuilt archive: ' . $url);
diff --git a/src/Container.php b/src/Container.php
index 3254a14..263fd41 100644
--- a/src/Container.php
+++ b/src/Container.php
@@ -5,9 +5,6 @@ declare(strict_types=1);
 namespace Php\Pie;
 
 use Composer\Util\Platform;
-use GuzzleHttp\Client;
-use GuzzleHttp\ClientInterface;
-use GuzzleHttp\RequestOptions;
 use Illuminate\Container\Container as IlluminateContainer;
 use Php\Pie\Building\Build;
 use Php\Pie\Building\UnixBuild;
@@ -58,12 +55,6 @@ final class Container
 
         $container->alias(ResolveDependencyWithComposer::class, DependencyResolver::class);
 
-        $container->bind(
-            ClientInterface::class,
-            static function (): ClientInterface {
-                return new Client([RequestOptions::HTTP_ERRORS => false]);
-            },
-        );
         $container->alias(GithubPackageReleaseAssets::class, PackageReleaseAssets::class);
         $container->when(GithubPackageReleaseAssets::class)
             ->needs('$githubApiBaseUrl')
diff --git a/src/Downloading/AddAuthenticationHeader.php b/src/Downloading/AddAuthenticationHeader.php
deleted file mode 100644
index ffd85f0..0000000
--- a/src/Downloading/AddAuthenticationHeader.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Php\Pie\Downloading;
-
-use Composer\Util\AuthHelper;
-use Php\Pie\DependencyResolver\Package;
-use Psr\Http\Message\RequestInterface;
-use RuntimeException;
-
-use function array_map;
-use function array_walk;
-use function count;
-use function explode;
-use function sprintf;
-
-/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
-final class AddAuthenticationHeader
-{
-    public static function withAuthHeaderFromComposer(RequestInterface $request, Package $package, AuthHelper $authHelper): RequestInterface
-    {
-        if ($package->downloadUrl === null) {
-            throw new RuntimeException(sprintf('The package %s does not have a download URL', $package->name));
-        }
-
-        $authHeaders = $authHelper->addAuthenticationHeader([], 'github.com', $package->downloadUrl);
-        array_walk(
-            $authHeaders,
-            static function (string $v) use (&$request): void {
-                $headerParts = array_map('trim', explode(':', $v, 2));
-
-                if (count($headerParts) !== 2 || ! $headerParts[0] || ! $headerParts[1]) {
-                    throw new RuntimeException('Authorization header is malformed, it should contain a non-empty key and a non-empty value.');
-                }
-
-                $request = $request->withHeader($headerParts[0], $headerParts[1]);
-            },
-        );
-
-        return $request;
-    }
-}
diff --git a/src/Downloading/AssertHttp.php b/src/Downloading/AssertHttp.php
deleted file mode 100644
index d4639d0..0000000
--- a/src/Downloading/AssertHttp.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Php\Pie\Downloading;
-
-use InvalidArgumentException;
-use Psr\Http\Message\ResponseInterface;
-
-use function sprintf;
-
-/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
-class AssertHttp
-{
-    /** Helper assertion that includes the HTTP response body when the HTTP status code does not match */
-    public static function responseStatusCode(int $expectedStatusCode, ResponseInterface $response): void
-    {
-        $actualStatusCode = $response->getStatusCode();
-        if ($actualStatusCode !== $expectedStatusCode) {
-            throw new InvalidArgumentException(sprintf(
-                'Expected HTTP %d response, got %d - response: %s',
-                $expectedStatusCode,
-                $actualStatusCode,
-                $response->getBody()->__toString(),
-            ));
-        }
-    }
-}
diff --git a/src/Downloading/GithubPackageReleaseAssets.php b/src/Downloading/GithubPackageReleaseAssets.php
index 677436d..69f338d 100644
--- a/src/Downloading/GithubPackageReleaseAssets.php
+++ b/src/Downloading/GithubPackageReleaseAssets.php
@@ -4,31 +4,23 @@ declare(strict_types=1);
 
 namespace Php\Pie\Downloading;
 
+use Composer\Downloader\TransportException;
 use Composer\Util\AuthHelper;
-use GuzzleHttp\ClientInterface;
-use GuzzleHttp\Psr7\Request;
-use GuzzleHttp\RequestOptions;
+use Composer\Util\HttpDownloader;
 use Php\Pie\DependencyResolver\Package;
 use Php\Pie\Platform\TargetPlatform;
 use Php\Pie\Platform\WindowsExtensionAssetName;
-use Psr\Http\Message\ResponseInterface;
 use Webmozart\Assert\Assert;
 
 use function array_map;
-use function assert;
 use function in_array;
-use function json_decode;
 use function strtolower;
 
-use const JSON_BIGINT_AS_STRING;
-use const JSON_THROW_ON_ERROR;
-
 /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
 final class GithubPackageReleaseAssets implements PackageReleaseAssets
 {
     /** @psalm-api */
     public function __construct(
-        private readonly ClientInterface $client,
         private readonly string $githubApiBaseUrl,
     ) {
     }
@@ -38,11 +30,12 @@ final class GithubPackageReleaseAssets implements PackageReleaseAssets
         TargetPlatform $targetPlatform,
         Package $package,
         AuthHelper $authHelper,
+        HttpDownloader $httpDownloader,
     ): string {
         $releaseAsset = $this->selectMatchingReleaseAsset(
             $targetPlatform,
             $package,
-            $this->getReleaseAssetsForPackage($package, $authHelper),
+            $this->getReleaseAssetsForPackage($package, $authHelper, $httpDownloader),
         );
 
         return $releaseAsset['browser_download_url'];
@@ -76,41 +69,33 @@ final class GithubPackageReleaseAssets implements PackageReleaseAssets
     }
 
     /** @return list<array{name: non-empty-string, browser_download_url: non-empty-string, ...}> */
-    private function getReleaseAssetsForPackage(Package $package, AuthHelper $authHelper): array
-    {
-        $request = AddAuthenticationHeader::withAuthHeaderFromComposer(
-            new Request('GET', $this->githubApiBaseUrl . '/repos/' . $package->githubOrgAndRepository() . '/releases/tags/' . $package->version),
-            $package,
-            $authHelper,
-        );
+    private function getReleaseAssetsForPackage(
+        Package $package,
+        AuthHelper $authHelper,
+        HttpDownloader $httpDownloader,
+    ): array {
+        Assert::notNull($package->downloadUrl);
 
-        $response = $this->client
-            ->sendAsync(
-                $request,
+        try {
+            $decodedRepsonse = $httpDownloader->get(
+                $this->githubApiBaseUrl . '/repos/' . $package->githubOrgAndRepository() . '/releases/tags/' . $package->version,
                 [
-                    RequestOptions::ALLOW_REDIRECTS => true,
-                    RequestOptions::HTTP_ERRORS => false,
-                    RequestOptions::SYNCHRONOUS => true,
+                    'retry-auth-failure' => false,
+                    'http' => [
+                        'method' => 'GET',
+                        'header' => $authHelper->addAuthenticationHeader([], $this->githubApiBaseUrl, $package->downloadUrl),
+                    ],
                 ],
-            )
-            ->wait();
-        assert($response instanceof ResponseInterface);
+            )->decodeJson();
+        } catch (TransportException $t) {
+            /** @link https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#get-a-release-by-tag-name */
+            if ($t->getStatusCode() === 404) {
+                throw Exception\CouldNotFindReleaseAsset::forPackageWithMissingTag($package);
+            }
 
-        /** @link https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#get-a-release-by-tag-name */
-        if ($response->getStatusCode() === 404) {
-            throw Exception\CouldNotFindReleaseAsset::forPackageWithMissingTag($package);
+            throw $t;
         }
 
-        AssertHttp::responseStatusCode(200, $response);
-
-        /** @var mixed $decodedRepsonse */
-        $decodedRepsonse = json_decode(
-            (string) $response->getBody(),
-            true,
-            512,
-            JSON_BIGINT_AS_STRING | JSON_THROW_ON_ERROR,
-        );
-
         Assert::isArray($decodedRepsonse);
         Assert::keyExists($decodedRepsonse, 'assets');
         Assert::isList($decodedRepsonse['assets']);
diff --git a/src/Downloading/PackageReleaseAssets.php b/src/Downloading/PackageReleaseAssets.php
index c1538e1..5f79cc3 100644
--- a/src/Downloading/PackageReleaseAssets.php
+++ b/src/Downloading/PackageReleaseAssets.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
 namespace Php\Pie\Downloading;
 
 use Composer\Util\AuthHelper;
+use Composer\Util\HttpDownloader;
 use Php\Pie\DependencyResolver\Package;
 use Php\Pie\Platform\TargetPlatform;
 
@@ -16,5 +17,6 @@ interface PackageReleaseAssets
         TargetPlatform $targetPlatform,
         Package $package,
         AuthHelper $authHelper,
+        HttpDownloader $httpDownloader,
     ): string;
 }


Test patch:
diff --git a/test/integration/Downloading/GithubPackageReleaseAssetsTest.php b/test/integration/Downloading/GithubPackageReleaseAssetsTest.php
new file mode 100644
index 0000000..ae0d56c
--- /dev/null
+++ b/test/integration/Downloading/GithubPackageReleaseAssetsTest.php
@@ -0,0 +1,70 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Php\PieIntegrationTest\Downloading;
+
+use Composer\Config;
+use Composer\IO\IOInterface;
+use Composer\Package\CompletePackage;
+use Composer\Util\AuthHelper;
+use Composer\Util\HttpDownloader;
+use Php\Pie\DependencyResolver\Package;
+use Php\Pie\Downloading\GithubPackageReleaseAssets;
+use Php\Pie\ExtensionName;
+use Php\Pie\ExtensionType;
+use Php\Pie\Platform\Architecture;
+use Php\Pie\Platform\OperatingSystem;
+use Php\Pie\Platform\TargetPhp\PhpBinaryPath;
+use Php\Pie\Platform\TargetPlatform;
+use Php\Pie\Platform\ThreadSafetyMode;
+use Php\Pie\Platform\WindowsCompiler;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(GithubPackageReleaseAssets::class)]
+final class GithubPackageReleaseAssetsTest extends TestCase
+{
+    public function testDeterminingReleaseAssetUrlForWindows(): void
+    {
+        $phpBinaryPath = $this->createMock(PhpBinaryPath::class);
+        $phpBinaryPath->expects(self::any())
+            ->method('majorMinorVersion')
+            ->willReturn('8.3');
+
+        $targetPlatform = new TargetPlatform(
+            OperatingSystem::Windows,
+            $phpBinaryPath,
+            Architecture::x86_64,
+            ThreadSafetyMode::ThreadSafe,
+            1,
+            WindowsCompiler::VS16,
+        );
+
+        $package = new Package(
+            $this->createMock(CompletePackage::class),
+            ExtensionType::PhpModule,
+            ExtensionName::normaliseFromString('example_pie_extension'),
+            'asgrim/example-pie-extension',
+            '2.0.2',
+            'https://api.github.com/repos/asgrim/example-pie-extension/zipball/f9ed13ea95dada34c6cc5a052da258dbda059d27',
+            [],
+            true,
+            true,
+        );
+
+        $io     = $this->createMock(IOInterface::class);
+        $config = new Config();
+
+        self::assertSame(
+            'https://github.com/asgrim/example-pie-extension/releases/download/2.0.2/php_example_pie_extension-2.0.2-8.3-ts-vs16-x86_64.zip',
+            (new GithubPackageReleaseAssets('https://api.github.com'))
+                ->findWindowsDownloadUrlForPackage(
+                    $targetPlatform,
+                    $package,
+                    new AuthHelper($io, $config),
+                    new HttpDownloader($io, $config),
+                ),
+        );
+    }
+}
diff --git a/test/unit/Downloading/AddAuthenticationHeaderTest.php b/test/unit/Downloading/AddAuthenticationHeaderTest.php
deleted file mode 100644
index ae23589..0000000
--- a/test/unit/Downloading/AddAuthenticationHeaderTest.php
+++ /dev/null
@@ -1,111 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Php\PieUnitTest\Downloading;
-
-use Composer\Package\CompletePackage;
-use Composer\Util\AuthHelper;
-use Generator;
-use GuzzleHttp\Psr7\Request;
-use Php\Pie\DependencyResolver\Package;
-use Php\Pie\Downloading\AddAuthenticationHeader;
-use Php\Pie\ExtensionName;
-use Php\Pie\ExtensionType;
-use PHPUnit\Framework\Attributes\CoversClass;
-use PHPUnit\Framework\Attributes\DataProvider;
-use PHPUnit\Framework\TestCase;
-use RuntimeException;
-
-use function uniqid;
-
-#[CoversClass(AddAuthenticationHeader::class)]
-final class AddAuthenticationHeaderTest extends TestCase
-{
-    public function testAuthorizationHeaderIsAdded(): void
-    {
-        $downloadUrl = 'http://test-uri/' . uniqid('path', true);
-
-        $request = new Request('GET', $downloadUrl);
-
-        $authHelper = $this->createMock(AuthHelper::class);
-        $authHelper->expects(self::once())
-            ->method('addAuthenticationHeader')
-            ->with([], 'github.com', $downloadUrl)
-            ->willReturn(['Authorization: whatever ABC123']);
-
-        $requestWithAuthHeader = (new AddAuthenticationHeader())->withAuthHeaderFromComposer(
-            $request,
-            $this->createDummyPackage($downloadUrl),
-            $authHelper,
-        );
-
-        self::assertSame('whatever ABC123', $requestWithAuthHeader->getHeaderLine('Authorization'));
-    }
-
-    #[DataProvider('provideInvalidAuthorizationHeaders')]
-    public function testEmptyValueInAuthorizationHeaderThrowsException(string $rawHeader): void
-    {
-        $downloadUrl = 'http://test-uri/' . uniqid('path', true);
-
-        $request = new Request('GET', $downloadUrl);
-
-        $authHelper = $this->createMock(AuthHelper::class);
-        $authHelper->expects(self::once())
-            ->method('addAuthenticationHeader')
-            ->with([], 'github.com', $downloadUrl)
-            ->willReturn([$rawHeader]);
-
-        $addAuthenticationHeader = new AddAuthenticationHeader();
-
-        $this->expectException(RuntimeException::class);
-        $this->expectExceptionMessage('Authorization header is malformed, it should contain a non-empty key and a non-empty value.');
-        $addAuthenticationHeader->withAuthHeaderFromComposer($request, $this->createDummyPackage($downloadUrl), $authHelper);
-    }
-
-    /**
-     * @return Generator<string[]>
-     *
-     * @psalm-suppress PossiblyUnusedMethod https://github.com/psalm/psalm-plugin-phpunit/issues/131
-     */
-    public static function provideInvalidAuthorizationHeaders(): Generator
-    {
-        yield ['Authorization:'];
-        yield [': Bearer'];
-        yield [' : Bearer'];
-        yield ['Authorization: '];
-        yield [':'];
-        yield ['Authorization MyToken'];
-    }
-
-    public function testExceptionIsThrownWhenPackageDoesNotHaveDownloadUrl(): void
-    {
-        $downloadUrl = 'http://test-uri/' . uniqid('path', true);
-
-        $request = new Request('GET', $downloadUrl);
-
-        $authHelper = $this->createMock(AuthHelper::class);
-
-        $addAuthenticationHeader = new AddAuthenticationHeader();
-        $package                 = $this->createDummyPackage();
-
-        $this->expectException(RuntimeException::class);
-        $this->expectExceptionMessage('The package foo/bar does not have a download URL');
-        $addAuthenticationHeader->withAuthHeaderFromComposer($request, $package, $authHelper);
-    }
-
-    private function createDummyPackage(string|null $downloadUrl = null): Package
-    {
-        return new Package(
-            $this->createMock(CompletePackage::class),
-            ExtensionType::PhpModule,
-            ExtensionName::normaliseFromString('foo'),
-            'foo/bar',
-            '1.2.3',
-            $downloadUrl,
-            [],
-            true,
-            true,
-        );
-    }
-}
diff --git a/test/unit/Downloading/AssertHttpTest.php b/test/unit/Downloading/AssertHttpTest.php
deleted file mode 100644
index 7568b51..0000000
--- a/test/unit/Downloading/AssertHttpTest.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Php\PieUnitTest\Downloading;
-
-use GuzzleHttp\Psr7\Response;
-use InvalidArgumentException;
-use Php\Pie\Downloading\AssertHttp;
-use PHPUnit\Framework\Attributes\CoversClass;
-use PHPUnit\Framework\TestCase;
-
-#[CoversClass(AssertHttp::class)]
-final class AssertHttpTest extends TestCase
-{
-    public function testResponseStatusCode(): void
-    {
-        $response = new Response(404, [], 'some body content');
-
-        $this->expectException(InvalidArgumentException::class);
-        $this->expectExceptionMessage('Expected HTTP 201 response, got 404 - response: some body content');
-        AssertHttp::responseStatusCode(201, $response);
-    }
-}
diff --git a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php
index 08ca52b..3be25fe 100644
--- a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php
+++ b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php
@@ -4,12 +4,11 @@ declare(strict_types=1);
 
 namespace Php\PieUnitTest\Downloading;
 
+use Composer\Downloader\TransportException;
 use Composer\Package\CompletePackage;
 use Composer\Util\AuthHelper;
-use GuzzleHttp\Client;
-use GuzzleHttp\Handler\MockHandler;
-use GuzzleHttp\HandlerStack;
-use GuzzleHttp\Psr7\Response;
+use Composer\Util\Http\Response;
+use Composer\Util\HttpDownloader;
 use Php\Pie\DependencyResolver\Package;
 use Php\Pie\Downloading\Exception\CouldNotFindReleaseAsset;
 use Php\Pie\Downloading\GithubPackageReleaseAssets;
@@ -24,7 +23,6 @@ use Php\Pie\Platform\WindowsCompiler;
 use PHPUnit\Framework\Attributes\CoversClass;
 use PHPUnit\Framework\TestCase;
 
-use function json_encode;
 use function uniqid;
 
 #[CoversClass(GithubPackageReleaseAssets::class)]
@@ -48,26 +46,28 @@ final class GithubPackageReleaseAssetsTest extends TestCase
 
         $authHelper = $this->createMock(AuthHelper::class);
 
-        $mockHandler = new MockHandler([
-            new Response(
-                200,
-                [],
-                json_encode([
-                    'assets' => [
-                        [
-                            'name' => 'php_foo-1.2.3-8.3-vc14-nts-x86.zip',
-                            'browser_download_url' => 'wrong_download_url',
-                        ],
-                        [
-                            'name' => 'php_foo-1.2.3-8.3-vc14-ts-x86.zip',
-                            'browser_download_url' => 'actual_download_url',
-                        ],
+        $httpDownloaderResponse = $this->createMock(Response::class);
+        $httpDownloaderResponse
+            ->expects(self::once())
+            ->method('decodeJson')
+            ->willReturn([
+                'assets' => [
+                    [
+                        'name' => 'php_foo-1.2.3-8.3-vc14-nts-x86.zip',
+                        'browser_download_url' => 'wrong_download_url',
                     ],
-                ]),
-            ),
-        ]);
+                    [
+                        'name' => 'php_foo-1.2.3-8.3-vc14-ts-x86.zip',
+                        'browser_download_url' => 'actual_download_url',
+                    ],
+                ],
+            ]);
 
-        $guzzleMockClient = new Client(['handler' => HandlerStack::create($mockHandler)]);
+        $httpDownloader = $this->createMock(HttpDownloader::class);
+        $httpDownloader
+            ->expects(self::once())
+            ->method('get')
+            ->willReturn($httpDownloaderResponse);
 
         $package = new Package(
             $this->createMock(CompletePackage::class),
@@ -81,9 +81,9 @@ final class GithubPackageReleaseAssetsTest extends TestCase
             true,
         );
 
-        $releaseAssets = new GithubPackageReleaseAssets($guzzleMockClient, 'https://test-github-api-base-url.thephp.foundation');
+        $releaseAssets = new GithubPackageReleaseAssets('https://test-github-api-base-url.thephp.foundation');
 
-        self::assertSame('actual_download_url', $releaseAssets->findWindowsDownloadUrlForPackage($targetPlatform, $package, $authHelper));
+        self::assertSame('actual_download_url', $releaseAssets->findWindowsDownloadUrlForPackage($targetPlatform, $package, $authHelper, $httpDownloader));
     }
 
     public function testUrlIsReturnedWhenFindingWindowsDownloadUrlWithCompilerAndThreadSafetySwapped(): void
@@ -104,26 +104,28 @@ final class GithubPackageReleaseAssetsTest extends TestCase
 
         $authHelper = $this->createMock(AuthHelper::class);
 
-        $mockHandler = new MockHandler([
-            new Response(
-                200,
-                [],
-                json_encode([
-                    'assets' => [
-                        [
-                            'name' => 'php_foo-1.2.3-8.3-nts-vc14-x86.zip',
-                            'browser_download_url' => 'wrong_download_url',
-                        ],
-                        [
-                            'name' => 'php_foo-1.2.3-8.3-ts-vc14-x86.zip',
-                            'browser_download_url' => 'actual_download_url',
-                        ],
+        $httpDownloaderResponse = $this->createMock(Response::class);
+        $httpDownloaderResponse
+            ->expects(self::once())
+            ->method('decodeJson')
+            ->willReturn([
+                'assets' => [
+                    [
+                        'name' => 'php_foo-1.2.3-8.3-nts-vc14-x86.zip',
+                        'browser_download_url' => 'wrong_download_url',
+                    ],
+                    [
+                        'name' => 'php_foo-1.2.3-8.3-ts-vc14-x86.zip',
+                        'browser_download_url' => 'actual_download_url',
                     ],
-                ]),
-            ),
-        ]);
+                ],
+            ]);
 
-        $guzzleMockClient = new Client(['handler' => HandlerStack::create($mockHandler)]);
+        $httpDownloader = $this->createMock(HttpDownloader::class);
+        $httpDownloader
+            ->expects(self::once())
+            ->method('get')
+            ->willReturn($httpDownloaderResponse);
 
         $package = new Package(
             $this->createMock(CompletePackage::class),
@@ -137,9 +139,9 @@ final class GithubPackageReleaseAssetsTest extends TestCase
             true,
         );
 
-        $releaseAssets = new GithubPackageReleaseAssets($guzzleMockClient, 'https://test-github-api-base-url.thephp.foundation');
+        $releaseAssets = new GithubPackageReleaseAssets('https://test-github-api-base-url.thephp.foundation');
 
-        self::assertSame('actual_download_url', $releaseAssets->findWindowsDownloadUrlForPackage($targetPlatform, $package, $authHelper));
+        self::assertSame('actual_download_url', $releaseAssets->findWindowsDownloadUrlForPackage($targetPlatform, $package, $authHelper, $httpDownloader));
     }
 
     public function testFindWindowsDownloadUrlForPackageThrowsExceptionWhenAssetNotFound(): void
@@ -155,17 +157,14 @@ final class GithubPackageReleaseAssetsTest extends TestCase
 
         $authHelper = $this->createMock(AuthHelper::class);
 
-        $mockHandler = new MockHandler([
-            new Response(
-                200,
-                [],
-                json_encode([
-                    'assets' => [],
-                ]),
-            ),
-        ]);
+        $e = new TransportException('not found', 404);
+        $e->setStatusCode(404);
 
-        $guzzleMockClient = new Client(['handler' => HandlerStack::create($mockHandler)]);
+        $httpDownloader = $this->createMock(HttpDownloader::class);
+        $httpDownloader
+            ->expects(self::once())
+            ->method('get')
+            ->willThrowException($e);
 
         $package = new Package(
             $this->createMock(CompletePackage::class),
@@ -179,9 +178,9 @@ final class GithubPackageReleaseAssetsTest extends TestCase
             true,
         );
 
-        $releaseAssets = new GithubPackageReleaseAssets($guzzleMockClient, 'https://test-github-api-base-url.thephp.foundation');
+        $releaseAssets = new GithubPackageReleaseAssets('https://test-github-api-base-url.thephp.foundation');
 
         $this->expectException(CouldNotFindReleaseAsset::class);
-        $releaseAssets->findWindowsDownloadUrlForPackage($targetPlatform, $package, $authHelper);
+        $releaseAssets->findWindowsDownloadUrlForPackage($targetPlatform, $package, $authHelper, $httpDownloader);
     }
 }