Skip to content
Merged
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
49 changes: 42 additions & 7 deletions src/Illuminate/Http/Client/PendingRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,13 @@ class PendingRequest
*/
protected $async = false;

/**
* The attributes to track with the request.
*
* @var array<array-key, mixed>
*/
protected $attributes = [];

/**
* The pending request promise.
*
Expand Down Expand Up @@ -700,6 +707,19 @@ public function withResponseMiddleware(callable $middleware)
return $this;
}

/**
* Set arbitrary attributes to store with the request.
*
* @param array<array-key, mixed> $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.
*
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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)
);

Expand All @@ -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();

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
);

Expand Down
30 changes: 30 additions & 0 deletions src/Illuminate/Http/Client/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ class Request implements ArrayAccess
*/
protected $data;

/**
* The attribute data passed when building the PendingRequest.
*
* @var array<array-key, mixed>
*/
protected $attributes = [];

/**
* Create a new request instance.
*
Expand Down Expand Up @@ -244,6 +251,29 @@ public function withData(array $data)
return $this;
}

/**
* Get the attribute data from the request.
*
* @return array<array-key, mixed>
*/
public function attributes()
{
return $this->attributes;
}

/**
* Set the request's attribute data.
*
* @param array<array-key, mixed> $attributes
* @return $this
*/
public function setRequestAttributes($attributes)
{
$this->attributes = $attributes;

return $this;
}

/**
* Get the underlying PSR compliant request instance.
*
Expand Down
22 changes: 22 additions & 0 deletions tests/Integration/Http/HttpClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}
}