Lately, I was missioned to integrate a vendor PHP SDK that is transport-agnostic as it implements HTTPlug. Pretty cool.

Everything went well until I found out that the SDK used a response reader method helper which is:

/**
 * @param ResponseInterface $response
 *
 * @return stdClass
 */
private function handleResponse(ResponseInterface $response)
{
    $this->setRateLimitDetails($response);

    $stream = $response->getBody()->getContents();

    return json_decode($stream);
}

First issue: I can't pick the response format, I'm forced to deal with the object deserialization version of the JSON payload. This is okay but not completely since the structure of the JSON may vary if my peers add ill-formatted custom attributes names.

Second issue: the method is private (like most of the non-public other ones 😔), which means I can't even reuse it if I decide to extend the class.

The third issue, which might not really be an issue: getting all the content of the body using $response->getBody()->getContents();.

I'll focus on this last point because I'm not really sure what to expect here. The ResponseInterface::getBody() returns a StreamInterface as defined by the PSR-7. However, if the usage of StreamInterface::getContents() seems quite legit here, I'm wondering if it should always assume the body hasn't been read before.

Back story

In my previous experiences, we used a lot Guzzle 3-4-5-6 and many middlewares with various responsibilities, one of them was to decode JSON bodies in order to log them for troubleshooting purposes.

For instance, the code looked like that:

/**
 * @return array<mixed>
 */
private function getLogData(RequestInterface $request, ?ResponseInterface $response): array
{
    $body = (string) $request->getBody();

    if (count($this->keyBlacklist)) {
        $body = $this->redactBlacklistedKeys($body);
    }

    $data = [
        'request' => [
            'url' => (string) $request->getUri(),
            'method' => $request->getMethod(),
            'body' => $body,
        ],
    ];

    if ($response) {
        $data['response'] = [
            'statusCode' => $response->getStatusCode(),
            // No data redacting here?
            'body' => (string) $response->getBody(),
        ];
    }

    return $data;
}
Good-enough implementation

This code is totally fine and worked as expected. The consumer code gets the response, then could easily decode its content:

/** @var array<mixed> */
$decoded = json_decode(
	(string) $response->getBody(),
    associative: true,
);

But if you get the content by using the StreamInterface::getContents() method, all you'll get is null. It is because the content has been read by the middleware and not rewound.

In the case when the message body is a Stream instance, we could see that the __toString() implementation rewinds the content before reading it, this is the reason why casting the body to string ((string) $response->getBody())   will always work.

If the middleware used the Message::rewindBody() method after reading the message content, it would have been transparent to the final body consumer.

use GuzzleHttp\Psr7\Message;

/**
 * @return array<mixed>
 */
private function getLogData(RequestInterface $request, ?ResponseInterface $response): array
{
    $body = (string) $request->getBody();

	// code
    
    Message::rewindBody($request);

	// code

    if ($response) {
		// code
        
        Message::rewindBody($response);
    }

    return $data;
}
Better implementation

When to use StreamInterface::getContents()?

Good question. In my opinion, it really depends on your needs. The PSR-7 states:

Finally, StreamInterface defines a __toString() method to simplify retrieving or emitting the entire body contents at once.

I think it speaks for itself: use StreamInterface::__toString() when you need the entire body with no hassle while you should use the underlying stream primitives (seek(), rewind(), tell(), eof(), etc.) when you need finer control of the content manipulation.

In the case of the previously-snippeted SDK, I believe a more robust version would be:

/**
 * @param ResponseInterface $response
 *
 * @return stdClass
 */
private function handleResponse(ResponseInterface $response)
{
    $this->setRateLimitDetails($response);

    return json_decode((string) $response->getBody());
}

This implementation is not better than the previous one, don't get me wrong. It's just more fail-safe when the underlying transport allows extensions like Guzzle.

In the case when you use middlewares you don't own AND these middlewares are not rewinding the body after reading its content, you have no other choice than prepending another middleware whose job is only to rewind bodies if you are not sure how the consumer code will get the content (joys of working with large and multi-background teams).

What do you think? I'd love to hear about your experiences with Guzzle and PSR-7 body manipulations.

Photo credits: https://unsplash.com/@lunarts

Joris Berthelot