diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index dcab5cb387c1..034d0213de44 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -195,6 +195,13 @@ class PendingRequest */ protected $async = false; + /** + * The attributes to track with the request. + * + * @var array + */ + protected $attributes = []; + /** * The pending request promise. * @@ -700,6 +707,19 @@ public function withResponseMiddleware(callable $middleware) return $this; } + /** + * Set arbitrary attributes to store with the request. + * + * @param array $attributes + * @return $this + */ + public function withAttributes($attributes) + { + $this->attributes = array_merge_recursive($this->attributes, $attributes); + + return $this; + } + /** * Add a new "before sending" callback to the request. * @@ -1123,7 +1143,10 @@ protected function makePromise(string $method, string $url, array $options = [], if ($e instanceof ConnectException || ($e instanceof RequestException && ! $e->hasResponse())) { $exception = new ConnectionException($e->getMessage(), 0, $e); - $this->dispatchConnectionFailedEvent(new Request($e->getRequest()), $exception); + $this->dispatchConnectionFailedEvent( + (new Request($e->getRequest()))->setRequestAttributes($this->attributes), + $exception + ); return $exception; } @@ -1399,7 +1422,9 @@ public function buildRecorderHandler() return $promise->then(function ($response) use ($request, $options) { $this->factory?->recordRequestResponsePair( - (new Request($request))->withData($options['laravel_data']), + (new Request($request)) + ->withData($options['laravel_data']) + ->setRequestAttributes($this->attributes), $this->newResponse($response) ); @@ -1420,7 +1445,12 @@ public function buildStubHandler() return function ($request, $options) use ($handler) { $response = ($this->stubCallbacks ?? new Collection) ->map - ->__invoke((new Request($request))->withData($options['laravel_data']), $options) + ->__invoke( + (new Request($request)) + ->withData($options['laravel_data']) + ->setRequestAttributes($this->attributes), + $options + ) ->filter() ->first(); @@ -1479,7 +1509,12 @@ public function runBeforeSendingCallbacks($request, array $options) return tap($request, function (&$request) use ($options) { $this->beforeSendingCallbacks->each(function ($callback) use (&$request, $options) { $callbackResult = call_user_func( - $callback, (new Request($request))->withData($options['laravel_data']), $options, $this + $callback, + (new Request($request)) + ->withData($options['laravel_data']) + ->setRequestAttributes($this->attributes), + $options, + $this ); if ($callbackResult instanceof RequestInterface) { @@ -1683,7 +1718,7 @@ protected function marshalConnectionException(ConnectException $e) { $exception = new ConnectionException($e->getMessage(), 0, $e); - $request = new Request($e->getRequest()); + $request = (new Request($e->getRequest()))->setRequestAttributes($this->attributes); $this->factory?->recordRequestResponsePair( $request, null @@ -1704,7 +1739,7 @@ protected function marshalRequestExceptionWithoutResponse(RequestException $e) { $exception = new ConnectionException($e->getMessage(), 0, $e); - $request = new Request($e->getRequest()); + $request = (new Request($e->getRequest()))->setRequestAttributes($this->attributes); $this->factory?->recordRequestResponsePair( $request, null @@ -1726,7 +1761,7 @@ protected function marshalRequestExceptionWithResponse(RequestException $e) $response = $this->populateResponse($this->newResponse($e->getResponse())); $this->factory?->recordRequestResponsePair( - new Request($e->getRequest()), + (new Request($e->getRequest()))->setRequestAttributes($this->attributes), $response ); diff --git a/src/Illuminate/Http/Client/Request.php b/src/Illuminate/Http/Client/Request.php index 7e6891221864..4878af66a4ac 100644 --- a/src/Illuminate/Http/Client/Request.php +++ b/src/Illuminate/Http/Client/Request.php @@ -26,6 +26,13 @@ class Request implements ArrayAccess */ protected $data; + /** + * The attribute data passed when building the PendingRequest. + * + * @var array + */ + protected $attributes = []; + /** * Create a new request instance. * @@ -244,6 +251,29 @@ public function withData(array $data) return $this; } + /** + * Get the attribute data from the request. + * + * @return array + */ + public function attributes() + { + return $this->attributes; + } + + /** + * Set the request's attribute data. + * + * @param array $attributes + * @return $this + */ + public function setRequestAttributes($attributes) + { + $this->attributes = $attributes; + + return $this; + } + /** * Get the underlying PSR compliant request instance. * diff --git a/tests/Integration/Http/HttpClientTest.php b/tests/Integration/Http/HttpClientTest.php index 7ff6719b6535..e9781e38e394 100644 --- a/tests/Integration/Http/HttpClientTest.php +++ b/tests/Integration/Http/HttpClientTest.php @@ -4,6 +4,7 @@ use Illuminate\Http\Client\Events\RequestSending; use Illuminate\Http\Client\Pool; +use Illuminate\Http\Client\Request; use Illuminate\Http\Client\Response; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Event; @@ -87,4 +88,25 @@ public function testForwardsCallsToPromise() $this->assertEquals('faked response', $myFakedResponse); $this->assertEquals('stub', $r); } + + public function testCanSetRequestAttributes() + { + Http::fake([ + '*' => fn (Request $request) => match($request->attributes()['name'] ?? null) { + 'first' => Http::response('first response'), + 'second' => Http::response('second response'), + default => Http::response('unnamed') + } + ]); + + $response1 = Http::withAttributes(['name' => 'first'])->get('https://some-store.myshopify.com/admin/api/2025-10/graphql.json'); + $response2 = Http::withAttributes(['name' => 'second'])->get('https://some-store.myshopify.com/admin/api/2025-10/graphql.json'); + $response3 = Http::get('https://some-store.myshopify.com/admin/api/2025-10/graphql.json'); + $response4 = Http::withAttributes(['name' => 'fourth'])->get('https://some-store.myshopify.com/admin/api/2025-10/graphql.json'); + + $this->assertEquals('first response', $response1->body()); + $this->assertEquals('second response', $response2->body()); + $this->assertEquals('unnamed', $response3->body()); + $this->assertEquals('unnamed', $response4->body()); + } }