Skip to content

Commit 2f8a29c

Browse files
Support Guzzle 8 and persistent connections across php-fpm requests
1 parent 3093ff3 commit 2f8a29c

10 files changed

Lines changed: 278 additions & 50 deletions

File tree

composer.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@
3232
"dragonmantank/cron-expression": "^3.4",
3333
"egulias/email-validator": "^4.0",
3434
"fruitcake/php-cors": "^1.3",
35-
"guzzlehttp/guzzle": "^7.8.2",
36-
"guzzlehttp/promises": "^2.0.3",
37-
"guzzlehttp/uri-template": "^1.0",
35+
"guzzlehttp/guzzle": "^7.8.2 || ^8.0",
36+
"guzzlehttp/promises": "^2.0.3 || ^3.0",
37+
"guzzlehttp/psr7": "^2.9.1 || ^3.0",
38+
"guzzlehttp/uri-template": "^1.0 || ^2.0",
3839
"laravel/prompts": "^0.3.0",
3940
"laravel/serializable-closure": "^2.0.10",
4041
"league/commonmark": "^2.8.1",
@@ -45,6 +46,7 @@
4546
"nesbot/carbon": "^3.8.4",
4647
"nunomaduro/termwind": "^2.0",
4748
"psr/container": "^1.1.1 || ^2.0.1",
49+
"psr/http-message": "^1.0 || ^2.0",
4850
"psr/log": "^1.0 || ^2.0 || ^3.0",
4951
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0",
5052
"ramsey/uuid": "^4.7",
@@ -71,7 +73,6 @@
7173
"ably/ably-php": "^1.0",
7274
"aws/aws-sdk-php": "^3.322.9",
7375
"fakerphp/faker": "^1.24",
74-
"guzzlehttp/psr7": "^2.9",
7576
"intervention/image": "^4.0",
7677
"laravel/pint": "^1.18",
7778
"league/flysystem-aws-s3-v3": "^3.25.1",
@@ -169,7 +170,6 @@
169170
"php-http/discovery": "Required to use PSR-7 bridging features (^1.15).",
170171
"phpunit/phpunit": "Required to use assertions and run tests (^11.5.50 || ^12.5.8 || ^13.0.3).",
171172
"predis/predis": "Required to use the predis connector (^2.3 || ^3.0).",
172-
"psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).",
173173
"pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0 || ^7.0).",
174174
"resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0 || ^1.0).",
175175
"spatie/fork": "Required to use the 'fork' concurrency driver (^1.2).",

src/Illuminate/Console/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"suggest": {
3131
"ext-pcntl": "Required to use signal trapping.",
3232
"dragonmantank/cron-expression": "Required to use scheduler (^3.3.2).",
33-
"guzzlehttp/guzzle": "Required to use the ping methods on schedules (^7.8).",
33+
"guzzlehttp/guzzle": "Required to use the ping methods on schedules (^7.8.2 || ^8.0).",
3434
"illuminate/bus": "Required to use the scheduled job dispatcher (^13.0).",
3535
"illuminate/container": "Required to use the scheduler (^13.0).",
3636
"illuminate/filesystem": "Required to use the generator command (^13.0).",

src/Illuminate/Filesystem/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).",
3232
"league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).",
3333
"league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).",
34-
"psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).",
34+
"psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0 || ^2.0).",
3535
"symfony/filesystem": "Required to enable support for relative symbolic links (^7.4 || ^8.0).",
3636
"symfony/mime": "Required to enable support for guessing extensions (^7.4 || ^8.0)."
3737
},

src/Illuminate/Http/Client/Factory.php

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ class Factory
4848
*/
4949
protected $globalOptions = [];
5050

51+
/**
52+
* The persistent transport (connection sharing) mode to apply to every request.
53+
*
54+
* @var \Illuminate\Http\Client\PersistentTransport
55+
*/
56+
protected PersistentTransport $globalPersistentTransport = PersistentTransport::None;
57+
5158
/**
5259
* The stub callables that will handle requests.
5360
*
@@ -154,6 +161,19 @@ public function globalOptions($options)
154161
return $this;
155162
}
156163

164+
/**
165+
* Set the persistent transport (connection sharing) mode to apply to every request.
166+
*
167+
* @param \Illuminate\Http\Client\PersistentTransport $mode
168+
* @return $this
169+
*/
170+
public function globalPersistentTransport(PersistentTransport $mode)
171+
{
172+
$this->globalPersistentTransport = $mode;
173+
174+
return $this;
175+
}
176+
157177
/**
158178
* Create a new response instance for use during stubbing.
159179
*
@@ -597,7 +617,9 @@ public function createPendingRequest()
597617
*/
598618
protected function newPendingRequest()
599619
{
600-
return (new PendingRequest($this, $this->globalMiddleware))->withOptions(value($this->globalOptions));
620+
return (new PendingRequest($this, $this->globalMiddleware))
621+
->withOptions(value($this->globalOptions))
622+
->persistentTransport($this->globalPersistentTransport);
601623
}
602624

603625
/**

src/Illuminate/Http/Client/PendingRequest.php

Lines changed: 126 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@
66
use Exception;
77
use GuzzleHttp\Client;
88
use GuzzleHttp\Cookie\CookieJar;
9-
use GuzzleHttp\Exception\ConnectException;
109
use GuzzleHttp\Exception\RequestException;
10+
use GuzzleHttp\Exception\ResponseException;
1111
use GuzzleHttp\Exception\TransferException;
1212
use GuzzleHttp\HandlerStack;
1313
use GuzzleHttp\Middleware;
1414
use GuzzleHttp\Promise\EachPromise;
1515
use GuzzleHttp\Promise\PromiseInterface;
16+
use GuzzleHttp\TransportSharing;
1617
use GuzzleHttp\UriTemplate\UriTemplate;
18+
use GuzzleHttp\Utils;
1719
use Illuminate\Contracts\Support\Arrayable;
1820
use Illuminate\Http\Client\Events\ConnectionFailed;
1921
use Illuminate\Http\Client\Events\RequestSending;
@@ -30,7 +32,9 @@
3032
use JsonSerializable;
3133
use Psr\Http\Message\MessageInterface;
3234
use Psr\Http\Message\RequestInterface;
35+
use Psr\Http\Message\ResponseInterface;
3336
use Psr\Http\Message\StreamInterface;
37+
use RuntimeException;
3438
use Symfony\Component\VarDumper\VarDumper;
3539
use Throwable;
3640

@@ -62,6 +66,13 @@ class PendingRequest
6266
*/
6367
protected $handler;
6468

69+
/**
70+
* The persistent transport (connection sharing) mode for the request.
71+
*
72+
* @var \Illuminate\Http\Client\PersistentTransport
73+
*/
74+
protected PersistentTransport $persistentTransport = PersistentTransport::None;
75+
6576
/**
6677
* The base URL for the request.
6778
*
@@ -1106,16 +1117,10 @@ public function send(string $method, string $url, array $options = [])
11061117
}
11071118
});
11081119
} catch (TransferException $e) {
1109-
if ($e instanceof ConnectException) {
1110-
$this->marshalConnectionException($e);
1111-
}
1112-
1113-
if ($e instanceof RequestException && ! $e->hasResponse()) {
1114-
$this->marshalRequestExceptionWithoutResponse($e);
1115-
}
1116-
1117-
if ($e instanceof RequestException && $e->hasResponse()) {
1118-
$this->marshalRequestExceptionWithResponse($e);
1120+
if (($response = $this->responseFromException($e)) !== null) {
1121+
$this->marshalTransportExceptionWithResponse($e, $response);
1122+
} else {
1123+
$this->marshalTransportException($e);
11191124
}
11201125

11211126
throw $e;
@@ -1137,6 +1142,10 @@ public function send(string $method, string $url, array $options = [])
11371142
*/
11381143
protected function expandUrlParameters(string $url)
11391144
{
1145+
if (! str_contains($url, '{')) {
1146+
return $url;
1147+
}
1148+
11401149
return UriTemplate::expand($url, $this->urlParameters);
11411150
}
11421151

@@ -1221,7 +1230,11 @@ protected function makePromise(string $method, string $url, array $options = [],
12211230
throw $e;
12221231
}
12231232

1224-
if ($e instanceof ConnectException || ($e instanceof RequestException && ! $e->hasResponse())) {
1233+
if (($response = $this->responseFromException($e)) !== null) {
1234+
return $this->populateResponse($this->newResponse($response));
1235+
}
1236+
1237+
if ($e instanceof TransferException) {
12251238
$exception = new ConnectionException($e->getMessage(), 0, $e);
12261239

12271240
$this->dispatchConnectionFailedEvent(
@@ -1232,7 +1245,7 @@ protected function makePromise(string $method, string $url, array $options = [],
12321245
return $exception;
12331246
}
12341247

1235-
return $e instanceof RequestException && $e->hasResponse() ? $this->populateResponse($this->newResponse($e->getResponse())) : $e;
1248+
return $e;
12361249
})
12371250
->then(function (Response|Throwable $response) use ($method, $url, $options, $attempt) {
12381251
return $this->handlePromiseResponse($response, $method, $url, $options, $attempt);
@@ -1255,8 +1268,9 @@ protected function handlePromiseResponse(Response|Throwable $response, $method,
12551268
return $response;
12561269
}
12571270

1258-
if ($response instanceof RequestException) {
1259-
$response = $this->populateResponse($this->newResponse($response->getResponse()));
1271+
if ($response instanceof RequestException
1272+
&& ($psrResponse = $this->responseFromException($response)) !== null) {
1273+
$response = $this->populateResponse($this->newResponse($psrResponse));
12601274
}
12611275

12621276
try {
@@ -1688,7 +1702,69 @@ public function createClient($handlerStack)
16881702
*/
16891703
public function buildHandlerStack()
16901704
{
1691-
return $this->pushHandlers(HandlerStack::create($this->handler));
1705+
return $this->pushHandlers(HandlerStack::create($this->buildDefaultHandler()));
1706+
}
1707+
1708+
/**
1709+
* Resolve the base Guzzle handler, applying persistent transport sharing when enabled.
1710+
*
1711+
* @return callable|null
1712+
*/
1713+
protected function buildDefaultHandler()
1714+
{
1715+
// Transport sharing can only be applied when Guzzle builds the handler.
1716+
if (! is_null($this->handler)) {
1717+
return $this->handler;
1718+
}
1719+
1720+
$mode = $this->resolveTransportSharingMode();
1721+
1722+
return is_null($mode)
1723+
? $this->handler
1724+
: Utils::chooseHandler(['transport_sharing' => $mode]);
1725+
}
1726+
1727+
/**
1728+
* Resolve the Guzzle "transport_sharing" mode for the configured persistence level.
1729+
*
1730+
* @return string|null
1731+
*
1732+
* @throws \RuntimeException
1733+
*/
1734+
protected function resolveTransportSharingMode()
1735+
{
1736+
if ($this->persistentTransport === PersistentTransport::None) {
1737+
return null;
1738+
}
1739+
1740+
// When faking, the stub handler answers before the base handler runs, so a
1741+
// sharing transport is never needed (and "Required" must not throw in tests).
1742+
if (($this->stubCallbacks?->isNotEmpty() ?? false) || $this->preventStrayRequests) {
1743+
return null;
1744+
}
1745+
1746+
$required = $this->persistentTransport === PersistentTransport::Required;
1747+
1748+
// Guzzle 8: persistent (cross-request) sharing.
1749+
if (defined(TransportSharing::class.'::PERSISTENT_PREFER')) {
1750+
return $required
1751+
? TransportSharing::PERSISTENT_REQUIRE
1752+
: TransportSharing::PERSISTENT_PREFER;
1753+
}
1754+
1755+
if ($required) {
1756+
throw new RuntimeException(
1757+
'Persistent HTTP transport sharing is set to "Required", but persistent cURL '
1758+
.'share handles require guzzlehttp/guzzle ^8.0.'
1759+
);
1760+
}
1761+
1762+
// Guzzle 7.11: handler-lifetime sharing only, best-effort.
1763+
if (class_exists(TransportSharing::class)) {
1764+
return TransportSharing::HANDLER_PREFER;
1765+
}
1766+
1767+
return null;
16921768
}
16931769

16941770
/**
@@ -2049,37 +2125,35 @@ public function dontTruncateExceptions()
20492125
}
20502126

20512127
/**
2052-
* Handle the given connection exception.
2128+
* Get the PSR-7 response carried by the given exception, if any.
20532129
*
2054-
* @param \GuzzleHttp\Exception\ConnectException $e
2055-
* @return void
2056-
*
2057-
* @throws \Illuminate\Http\Client\ConnectionException
2130+
* @param \Throwable $e
2131+
* @return \Psr\Http\Message\ResponseInterface|null
20582132
*/
2059-
protected function marshalConnectionException(ConnectException $e)
2133+
protected function responseFromException(Throwable $e)
20602134
{
2061-
$exception = new ConnectionException($e->getMessage(), 0, $e);
2062-
2063-
$request = (new Request($e->getRequest()))->setRequestAttributes($this->attributes);
2064-
2065-
$this->factory?->recordRequestResponsePair(
2066-
$request, null
2067-
);
2135+
// Guzzle 8 uses ResponseException
2136+
if ($e instanceof ResponseException) {
2137+
return $e->getResponse();
2138+
}
20682139

2069-
$this->dispatchConnectionFailedEvent($request, $exception);
2140+
// Guzzle 7 uses RequestException with hasResponse() true
2141+
if ($e instanceof RequestException && is_callable([$e, 'hasResponse']) && $e->hasResponse()) {
2142+
return $e->getResponse();
2143+
}
20702144

2071-
throw $exception;
2145+
return null;
20722146
}
20732147

20742148
/**
2075-
* Handle the given request exception.
2149+
* Handle the given transport exception.
20762150
*
2077-
* @param \GuzzleHttp\Exception\RequestException $e
2151+
* @param \GuzzleHttp\Exception\TransferException $e
20782152
* @return void
20792153
*
20802154
* @throws \Illuminate\Http\Client\ConnectionException
20812155
*/
2082-
protected function marshalRequestExceptionWithoutResponse(RequestException $e)
2156+
protected function marshalTransportException(TransferException $e)
20832157
{
20842158
$exception = new ConnectionException($e->getMessage(), 0, $e);
20852159

@@ -2095,17 +2169,18 @@ protected function marshalRequestExceptionWithoutResponse(RequestException $e)
20952169
}
20962170

20972171
/**
2098-
* Handle the given request exception.
2172+
* Handle the given transport exception that carried a response.
20992173
*
2100-
* @param \GuzzleHttp\Exception\RequestException $e
2174+
* @param \GuzzleHttp\Exception\TransferException $e
2175+
* @param \Psr\Http\Message\ResponseInterface $response
21012176
* @return void
21022177
*
21032178
* @throws \Illuminate\Http\Client\RequestException
21042179
* @throws \Illuminate\Http\Client\ConnectionException
21052180
*/
2106-
protected function marshalRequestExceptionWithResponse(RequestException $e)
2181+
protected function marshalTransportExceptionWithResponse(TransferException $e, ResponseInterface $response)
21072182
{
2108-
$response = $this->populateResponse($this->newResponse($e->getResponse()));
2183+
$response = $this->populateResponse($this->newResponse($response));
21092184

21102185
$this->factory?->recordRequestResponsePair(
21112186
(new Request($e->getRequest()))->setRequestAttributes($this->attributes),
@@ -2141,6 +2216,19 @@ public function setHandler($handler)
21412216
return $this;
21422217
}
21432218

2219+
/**
2220+
* Set the persistent transport (connection sharing) mode for the request.
2221+
*
2222+
* @param \Illuminate\Http\Client\PersistentTransport $mode
2223+
* @return $this
2224+
*/
2225+
public function persistentTransport(PersistentTransport $mode)
2226+
{
2227+
$this->persistentTransport = $mode;
2228+
2229+
return $this;
2230+
}
2231+
21442232
/**
21452233
* Get the pending request options.
21462234
*
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Illuminate\Http\Client;
4+
5+
enum PersistentTransport: string
6+
{
7+
case None = 'none';
8+
case Preferred = 'preferred';
9+
case Required = 'required';
10+
}

src/Illuminate/Http/Client/Promises/FluentPromise.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public function otherwise(callable $onRejected): PromiseInterface
3434
}
3535

3636
#[\Override]
37-
public function resolve($value): void
37+
public function resolve($value = null): void
3838
{
3939
$this->guzzlePromise->resolve($value);
4040
}

0 commit comments

Comments
 (0)