Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[9.x] Allow faking connection errors in HTTP client #44852

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 17 additions & 19 deletions src/Illuminate/Http/Client/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
namespace Illuminate\Http\Client;

use Closure;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Psr7\Response as Psr7Response;
use GuzzleHttp\TransferStats;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Macroable;
Expand Down Expand Up @@ -140,6 +139,20 @@ public static function response($body = null, $status = 200, $headers = [])
: \GuzzleHttp\Promise\promise_for($response);
}

/**
* Create a new connection exception for use during stubbing.
*
* @param string $message
* @param array $handlerContext
* @return callable(\Illuminate\Http\Client\Request): \GuzzleHttp\Exception\ConnectException
*/
public static function error($message = '', array $handlerContext = [])
{
return function (Request $request) use ($message, $handlerContext) {
return new ConnectException($message, $request->toPsrRequest(), null, $handlerContext);
};
}

/**
* Get an invokable object that returns a sequence of responses in order for use during stubbing.
*
Expand Down Expand Up @@ -177,22 +190,7 @@ public function fake($callback = null)
return $this;
}

$this->stubCallbacks = $this->stubCallbacks->merge(collect([
function ($request, $options) use ($callback) {
$response = $callback instanceof Closure
? $callback($request, $options)
: $callback;

if ($response instanceof PromiseInterface) {
$options['on_stats'](new TransferStats(
$request->toPsrRequest(),
$response->wait(),
));
}

return $response;
},
]));
$this->stubCallbacks->add($callback);

return $this;
}
Expand Down Expand Up @@ -269,7 +267,7 @@ protected function record()
* Record a request response pair.
*
* @param \Illuminate\Http\Client\Request $request
* @param \Illuminate\Http\Client\Response $response
* @param \Illuminate\Http\Client\Response|\Illuminate\Http\Client\ConnectionException $response
* @return void
*/
public function recordRequestResponsePair($request, $response)
Expand Down
50 changes: 43 additions & 7 deletions src/Illuminate/Http/Client/PendingRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Exception\TransferException;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\TransferStats;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Http\Client\Events\ConnectionFailed;
use Illuminate\Http\Client\Events\RequestSending;
Expand Down Expand Up @@ -87,7 +88,7 @@ class PendingRequest
/**
* The transfer stats for the request.
*
* \GuzzleHttp\TransferStats
* @var \GuzzleHttp\TransferStats
*/
protected $transferStats;

Expand Down Expand Up @@ -888,7 +889,20 @@ protected function makePromise(string $method, string $url, array $options = [])
});
})
->otherwise(function (TransferException $e) {
return $e instanceof RequestException && $e->hasResponse() ? $this->populateResponse(new Response($e->getResponse())) : $e;
if ($e instanceof RequestException && $e->hasResponse()) {
return tap(new Response($e->getResponse()), function ($response) {
$this->populateResponse($response);
$this->dispatchResponseReceivedEvent($response);
});
}

if ($e instanceof ConnectException) {
return tap(new ConnectionException($e->getMessage(), 0, $e), function () {
$this->dispatchConnectionFailedEvent();
});
}

return $e;
});
}

Expand Down Expand Up @@ -1072,6 +1086,17 @@ public function buildRecorderHandler()
);

return $response;
}, function ($reason) use ($request, $options) {
if ($reason instanceof ConnectException) {
$this->factory?->recordRequestResponsePair(
(new Request($request))->withData($options['laravel_data']),
new ConnectionException($reason->getMessage(), 0, $reason)
);
}

return class_exists(\GuzzleHttp\Promise\Create::class)
? \GuzzleHttp\Promise\Create::rejectionFor($reason)
: \GuzzleHttp\Promise\rejection_for($reason);
});
};
};
Expand All @@ -1086,11 +1111,11 @@ public function buildStubHandler()
{
return function ($handler) {
return function ($request, $options) use ($handler) {
$response = ($this->stubCallbacks ?? collect())
->map
->__invoke((new Request($request))->withData($options['laravel_data']), $options)
->filter()
->first();
$response = ($this->stubCallbacks ?? collect())->reduce(
fn ($response, $callable) => $response ?? $callable(
(new Request($request))->withData($options['laravel_data']),
$options
));

if (is_null($response)) {
if ($this->preventStrayRequests) {
Expand All @@ -1100,8 +1125,19 @@ public function buildStubHandler()
return $handler($request, $options);
}

if ($response instanceof ConnectException) {
return class_exists(\GuzzleHttp\Promise\Create::class)
? \GuzzleHttp\Promise\Create::rejectionFor($response)
: \GuzzleHttp\Promise\rejection_for($response);
}

$response = is_array($response) ? Factory::response($response) : $response;

$options['on_stats'](new TransferStats(
$request,
$response->wait(),
));

$sink = $options['sink'] ?? null;

if ($sink) {
Expand Down
2 changes: 1 addition & 1 deletion src/Illuminate/Http/Client/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class Response implements ArrayAccess
/**
* The transfer stats for the request.
*
* \GuzzleHttp\TransferStats|null
* @var \GuzzleHttp\TransferStats|null
*/
public $transferStats;

Expand Down
24 changes: 20 additions & 4 deletions src/Illuminate/Http/Client/ResponseSequence.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public function pushStatus(int $status, array $headers = [])
}

/**
* Push response with the contents of a file as the body to the sequence.
* Push a response with the contents of a file as the body to the sequence.
*
* @param string $filePath
* @param int $status
Expand All @@ -87,6 +87,20 @@ public function pushFile(string $filePath, int $status = 200, array $headers = [
);
}

/**
* Push a connection exception to the sequence.
*
* @param string $message
* @param array $handlerContext
* @return $this
*/
public function pushError(string $message = '', array $handlerContext = [])
{
return $this->pushResponse(
Factory::error($message, $handlerContext)
);
}

/**
* Push a response to the sequence.
*
Expand Down Expand Up @@ -137,20 +151,22 @@ public function isEmpty()
/**
* Get the next response in the sequence.
*
* @param \Illuminate\Http\Client\Request $request
* @param array $options
* @return mixed
*
* @throws \OutOfBoundsException
*/
public function __invoke()
public function __invoke(Request $request, array $options)
{
if ($this->failWhenEmpty && count($this->responses) === 0) {
throw new OutOfBoundsException('A request was made, but the response sequence is empty.');
}

if (! $this->failWhenEmpty && count($this->responses) === 0) {
return value($this->emptyResponse ?? Factory::response());
return value($this->emptyResponse ?? Factory::response(), $request, $options);
}

return array_shift($this->responses);
return value(array_shift($this->responses), $request, $options);
}
}
1 change: 1 addition & 0 deletions src/Illuminate/Support/Facades/Http.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

/**
* @method static \GuzzleHttp\Promise\PromiseInterface response($body = null, $status = 200, $headers = [])
* @method static callable error(string $message = '', array $handlerContext = [])
* @method static \Illuminate\Http\Client\PendingRequest accept(string $contentType)
* @method static \Illuminate\Http\Client\PendingRequest acceptJson()
* @method static \Illuminate\Http\Client\PendingRequest asForm()
Expand Down
Loading