66use Exception ;
77use GuzzleHttp \Client ;
88use GuzzleHttp \Cookie \CookieJar ;
9- use GuzzleHttp \Exception \ConnectException ;
109use GuzzleHttp \Exception \RequestException ;
10+ use GuzzleHttp \Exception \ResponseException ;
1111use GuzzleHttp \Exception \TransferException ;
1212use GuzzleHttp \HandlerStack ;
1313use GuzzleHttp \Middleware ;
1414use GuzzleHttp \Promise \EachPromise ;
1515use GuzzleHttp \Promise \PromiseInterface ;
16+ use GuzzleHttp \TransportSharing ;
1617use GuzzleHttp \UriTemplate \UriTemplate ;
18+ use GuzzleHttp \Utils ;
1719use Illuminate \Contracts \Support \Arrayable ;
1820use Illuminate \Http \Client \Events \ConnectionFailed ;
1921use Illuminate \Http \Client \Events \RequestSending ;
3032use JsonSerializable ;
3133use Psr \Http \Message \MessageInterface ;
3234use Psr \Http \Message \RequestInterface ;
35+ use Psr \Http \Message \ResponseInterface ;
3336use Psr \Http \Message \StreamInterface ;
37+ use RuntimeException ;
3438use Symfony \Component \VarDumper \VarDumper ;
3539use 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 *
0 commit comments