HTTP trailers are header fields that servers send after the response body.
They are useful when a value is only known once the whole body has been generated,
like a checksum of the content or the final status of a streamed operation. The
best-known example is gRPC, which sends the result of each call in the
grpc-status and grpc-message trailers. Until now, the HttpClient component
discarded them silently.
Reading HTTP Trailers
In Symfony 8.2, responses expose the trailers in a new trailers info item.
Its value is null until the transfer completes successfully. Then, it's an
empty array when the server sent no trailers, or an array with the same shape as
getHeaders(): field names in lowercase, each one mapped to the list of its values:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
$response = $client->request('POST', 'https://grpc.example.com/shop.Orders/Create', [
'http_version' => '2.0',
// gRPC requires 'te: trailers' to tell the server that the client accepts trailers
'headers' => ['content-type' => 'application/grpc', 'te' => 'trailers'],
'body' => $grpcFrame,
]);
// trailers are only available after reading the entire response
$content = $response->getContent();
// e.g. ['grpc-status' => ['0'], 'grpc-message' => ['']]
$trailers = $response->getInfo('trailers');
// gRPC "trailers-only" responses (e.g. errors with no body) send the status as a header
$status = $trailers['grpc-status'][0] ?? $response->getHeaders(false)['grpc-status'][0];
if ('0' !== $status) {
// the gRPC call failed ...
}
Trailers work with the cURL, native and Amp clients, and you can also set them
in the $info argument of MockResponse in your tests. Symfony never merges
trailers into the regular response headers, so getHeaders() returns the
same values as before.
Trailers in the Guzzle Handler
The GuzzleHttpHandler lets applications and libraries built on Guzzle send
their requests through the Symfony HttpClient. In Symfony 8.2, this handler also
supports Guzzle's on_trailers request option, which runs a callback when the
transfer completes:
1 2 3 4 5 6 7 8 9 10 11 12 13
use GuzzleHttp\Client;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\HttpClient\GuzzleHttpHandler;
$guzzle = new Client(['handler' => new GuzzleHttpHandler($httpClient)]);
$guzzle->post('https://grpc.example.com/shop.Orders/Create', [
// ...
'on_trailers' => function (array $trailers, ResponseInterface $response, RequestInterface $request): void {
$this->logger->info('gRPC status: {status}', ['status' => $trailers['grpc-status'][0] ?? 'n/a']);
},
]);
If the underlying Symfony HTTP client doesn't expose trailers, the request fails instead of silently never calling your callback.