Log outbound Guzzle calls with middleware instead of creating a second request object. Middleware receives the prepared request, delegates it to the existing handler, and observes the response promise. Existing request options continue to work.
Last updated: October 3, 2026.
<?php
use GuzzleHttpClient;
use GuzzleHttpHandlerStack;
use GuzzleHttpPromiseCreate;
use PsrHttpMessageRequestInterface;
use PsrHttpMessageResponseInterface;
$writeLog = static function (array $entry): void {
error_log(json_encode($entry, JSON_THROW_ON_ERROR));
};
$stack = HandlerStack::create();
$stack->push(function (callable $handler) use ($writeLog) {
return function (RequestInterface $request, array $options) use ($handler, $writeLog) {
$started = microtime(true);
return $handler($request, $options)->then(
function (ResponseInterface $response) use ($request, $started, $writeLog) {
$writeLog([
'method' => $request->getMethod(),
'path' => $request->getUri()->getPath(),
'status' => $response->getStatusCode(),
'duration_ms' => round((microtime(true) - $started) * 1000),
]);
return $response;
},
function ($reason) use ($request, $started, $writeLog) {
$writeLog([
'method' => $request->getMethod(),
'path' => $request->getUri()->getPath(),
'error' => $reason instanceof Throwable ? $reason->getMessage() : 'Request failed',
'duration_ms' => round((microtime(true) - $started) * 1000),
]);
return Create::rejectionFor($reason);
}
);
};
});
$client = new Client(['handler' => $stack, 'base_uri' => 'https://api.example.com/']);
$response = $client->post('orders', ['json' => ['product_id' => 42]]);The middleware returns the same response or rejection. Replace error_log() with your application logger while keeping the record structured.
Log metadata, not credentials or payloads
Method, path, status, duration, retry count, and a correlation ID are usually enough. Do not log Authorization, cookies, API keys, access tokens, or complete bodies. Query strings can contain secrets too, so the example records only getPath().
Guzzle’s handler and middleware documentation defines this promise-based wrapper model and explains why HandlerStack::create() preserves the default middleware.
Handle failures without swallowing them
The rejection callback records the failure and returns another rejected promise. Returning a successful value there would accidentally convert the failed request into a fulfilled one. Keep logging itself lightweight and resilient so a logging outage does not block an API call.
For adjacent integrations, see sending transactional email through an HTTP API, PHP functions and return types, and reliable webhook delivery.