diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index f1fd87be3f22..e75992360d6c 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -165,6 +165,13 @@ class PendingRequest */ protected $beforeSendingCallbacks; + /** + * The callbacks that should execute after the Laravel Response is built. + * + * @var \Illuminate\Support\Collection + */ + protected $afterResponseCallbacks; + /** * The stub callables that will handle requests. * @@ -268,6 +275,8 @@ public function __construct(?Factory $factory = null, $middleware = []) $pendingRequest->dispatchRequestSendingEvent(); }]); + + $this->afterResponseCallbacks = new Collection(); } /** @@ -738,6 +747,19 @@ public function beforeSending($callback) }); } + /** + * Add a new callback to execute after the response is built. + * + * @param (callable(\Illuminate\Http\Client\Response): \Illuminate\Http\Client\Response|null) $callback + * @return $this + */ + public function afterResponse(callable $callback) + { + $this->afterResponseCallbacks[] = $callback; + + return $this; + } + /** * Throw an exception if a server or client error occurs. * @@ -1004,10 +1026,11 @@ public function send(string $method, string $url, array $options = []) return retry($this->tries ?? 1, function ($attempt) use ($method, $url, $options, &$shouldRetry) { try { - return tap($this->newResponse($this->sendRequest($method, $url, $options)), function ($response) use ($attempt, &$shouldRetry) { + return tap($this->newResponse($this->sendRequest($method, $url, $options)), function (&$response) use ($attempt, &$shouldRetry) { $this->populateResponse($response); $this->dispatchResponseReceivedEvent($response); + $response = $this->runAfterResponseCallbacks($response); if ($response->successful()) { return; @@ -1150,10 +1173,12 @@ protected function makePromise(string $method, string $url, array $options = [], { return $this->promise = $this->sendRequest($method, $url, $options) ->then(function (MessageInterface $message) { - return tap($this->newResponse($message), function ($response) { - $this->populateResponse($response); - $this->dispatchResponseReceivedEvent($response); - }); + $response = $this->newResponse($message); + + $this->populateResponse($response); + $this->dispatchResponseReceivedEvent($response); + + return $this->runAfterResponseCallbacks($response); }) ->otherwise(function (OutOfBoundsException|TransferException|StrayRequestException $e) { if ($e instanceof StrayRequestException) { @@ -1520,9 +1545,9 @@ protected function sinkStubHandler($sink) /** * Execute the "before sending" callbacks. * - * @param \GuzzleHttp\Psr7\RequestInterface $request + * @param \Psr\Http\Message\RequestInterface $request * @param array $options - * @return \GuzzleHttp\Psr7\RequestInterface + * @return \Psr\Http\Message\RequestInterface */ public function runBeforeSendingCallbacks($request, array $options) { @@ -1579,6 +1604,25 @@ protected function newResponse($response) }); } + /** + * Execute the "after response" callbacks. + * + * @param \Illuminate\Http\Client\Response $response + * @return \Illuminate\Http\Client\Response + */ + protected function runAfterResponseCallbacks(Response $response) + { + foreach ($this->afterResponseCallbacks as $callback) { + $returnedResponse = $callback($response); + + if ($returnedResponse instanceof Response) { + $response = $returnedResponse; + } + } + + return $response; + } + /** * Register a stub callable that will intercept requests and be able to return stub responses. * diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index cd6cf6313b44..0ac82ae526b8 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -4194,6 +4194,60 @@ public static function methodsReceivingArrayableDataProvider() 'delete' => ['delete'], ]; } + + public function testAfterResponse() + { + $this->factory->fake([ + 'http://200.com*' => $this->factory::response('OK'), + ]); + + $response = $this->factory + ->afterResponse(fn (Response $response): TestResponse => new TestResponse($response->toPsrResponse())) + ->afterResponse(fn () => 'abc') + ->afterResponse(function ($r) { + $this->assertInstanceOf(TestResponse::class, $r); + }) + ->afterResponse(fn (Response $r) => new Response($r->toPsrResponse()->withBody(Utils::streamFor(strtolower($r->body()))))) + ->get('http://200.com'); + + $this->assertInstanceOf(Response::class, $response); + $this->assertSame('ok', $response->body()); + } + + public function testAfterResponseWithThrows() + { + $this->factory->fake([ + 'http://500.com*' => $this->factory::response('oh no', 500), + ]); + + try { + $this->factory->throw() + ->afterResponse(fn ($response) => new TestResponse($response->toPsrResponse())) + ->post('http://500.com'); + } catch (RequestException $e) { + $this->assertInstanceOf(TestResponse::class, $e->response); + } + } + + public function testAfterResponseWithAsync() + { + $this->factory->fake([ + 'http://200.com*' => $this->factory::response('OK', 200), + 'http://401.com*' => $this->factory::response('Unauthorized.', 401), + ]); + + $o = $this->factory->pool(function (Pool $pool): void { + $pool->as('200')->afterResponse(fn (Response $response) => new TestResponse($response->toPsrResponse()))->get('http://200.com'); + $pool->as('401-throwing')->throw()->afterResponse(fn (Response $response) => new TestResponse($response->toPsrResponse()))->get('http://401.com'); + $pool->as('401-response')->afterResponse(fn (Response $response) => new TestResponse($response->toPsrResponse()->withBody(Utils::streamFor('different'))))->get('http://401.com'); + }, 0); + + $this->assertInstanceOf(TestResponse::class, $o['200']); + $this->assertInstanceOf(TestResponse::class, $o['401-response']); + $this->assertEquals('different', $o['401-response']->body()); + $this->assertInstanceOf(RequestException::class, $o['401-throwing']); + $this->assertInstanceOf(TestResponse::class, $o['401-throwing']->response); + } } class CustomFactory extends Factory