// SPDX-FileCopyrightText: 2004-2023 Ryan Parman, Sam Sneddon, Ryan McCue
// SPDX-License-Identifier: BSD-3-Clause
namespace SimplePie\HTTP;
use InvalidArgumentException;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\UriFactoryInterface;
* HTTP Client based on PSR-18 and PSR-17 implementations
final class Psr18Client implements Client
* @var RequestFactoryInterface
* @var UriFactoryInterface
private $allowedRedirects = 5;
public function __construct(ClientInterface $httpClient, RequestFactoryInterface $requestFactory, UriFactoryInterface $uriFactory)
$this->httpClient = $httpClient;
$this->requestFactory = $requestFactory;
$this->uriFactory = $uriFactory;
public function getHttpClient(): ClientInterface
return $this->httpClient;
public function getRequestFactory(): RequestFactoryInterface
return $this->requestFactory;
public function getUriFactory(): UriFactoryInterface
return $this->uriFactory;
* send a request and return the response
* @param Client::METHOD_* $method
* @param array<string,string|string[]> $headers
* @throws ClientException if anything goes wrong requesting the data
public function request(string $method, string $url, array $headers = []): Response
if ($method !== self::METHOD_GET) {
throw new InvalidArgumentException(sprintf(
'%s(): Argument #1 ($method) only supports method "%s".',
if (preg_match('/^http(s)?:\/\//i', $url)) {
return $this->requestUrl($method, $url, $headers);
return $this->requestLocalFile($url);
* @param array<string,string|string[]> $headers
private function requestUrl(string $method, string $url, array $headers): Response
$remainingRedirects = $this->allowedRedirects;
$request = $this->requestFactory->createRequest(
$this->uriFactory->createUri($requestedUrl)
foreach ($headers as $name => $value) {
$request = $request->withHeader($name, $value);
$response = $this->httpClient->sendRequest($request);
} catch (ClientExceptionInterface $th) {
throw new ClientException($th->getMessage(), $th->getCode(), $th);
$statusCode = $response->getStatusCode();
if (in_array($statusCode, [300, 301, 302, 303, 307]) && $response->hasHeader('Location')) {
// Prevent infinity redirect loops
if ($remainingRedirects <= 0) {
$requestedUrl = $response->getHeaderLine('Location');
if ($statusCode === 301) {
$permanentUrl = $requestedUrl;
$request = $request->withUri($this->uriFactory->createUri($requestedUrl));
} while ($followRedirect);
return new Psr7Response($response, $permanentUrl, $requestedUrl);
private function requestLocalFile(string $path): Response
if (!is_readable($path)) {
throw new ClientException(sprintf('file "%s" is not readable', $path));
$raw = file_get_contents($path);
} catch (Throwable $th) {
throw new ClientException($th->getMessage(), $th->getCode(), $th);
throw new ClientException('file_get_contents() could not read the file', 1);
return new RawTextResponse($raw, $path);