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

Grégory PLANCHAT
Contributed by Grégory PLANCHAT in #66256

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

Nicolas Grekas
Contributed by Nicolas Grekas in #66263

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.

Published in #Living on the edge