PSR-7 Objects Are Not Immutable

May 22, 2016

Wait, what!? But it says they are, right here:

“Why value objects? The proposal models messages and URIs as value objects. Messages are values where the identity is the aggregate of all parts of the message; a change to any aspect of the message is essentially a new message. This is the very definition of a value object. The practice by which changes result in a new instance is termed immutability, and is a feature designed to ensure the integrity of a given value.”

An Experiment

If you were to install the Zend Expressive skeleton (v1.0.0) and register an action that does the following:

$action = function ($rq, $rs, $next = null) {
    $response->getBody()->write('You should not see this message');
    $response = $response->withHeader('Content-Type', 'application/json');

    throw new \Exception('Access denied');

    return $response;
}

You shouldn’t expect to see the message that was written to the response actually rendered. The code we have written explicitly avoided returning the response by throwing the exception. However, if you disable the Whoops error handler and use the default templated error handler you will find that your message still appears at the top of the error page that is generated by the framework.

What’s Happening Here?

What’s happening is that the Zend Expressive framework is rendering the error page to the same object that you wrote your message to. Whilst the actual message object itself is immutable, the body stream that it references is not. Even when this object is cloned or “modified” (to become a new object) it will still use the same stream.

What Does This Mean

What this means is that we lose the core benefit of immutability. That is, we can not trust that the state of our PSR-7 objects cannot be modified when passed to external actors. Alternatively put, we cannot treat these objects as if they were completely immutable value objects.

This topic was actually well discussed whilst PSR-7 was being developed. The meta-document even contains a section explaining the design decision behind why streams in PSR-7 are mutable.

Regardless of the information out there, it appears that there is a lot of code being written that is making this false assumption of immutability.

The Middleware Debate

At the moment, there is an on-going debate about the standardisation of PSR-7 middleware. Two important blog posts have been written about the topic, both of them make the mistake of assuming that PSR-7 objects are completely immutable.

In the post by Anthony Ferrara, he offers the following as an alternative to passing a response instance through to the handle() method:

class MyMiddleware implements Middleware {
    private $response;
    public function __construct(ResponseInterface $response) {
        $this->response = $response;
    }
    public function handle(
        RequestInterface $request,
        Frame $frame
    ): ResponseInterface {
        return $this->response->withStatusCode(404);
    }
}

For this code to work, the response object would need to be a suitable factory for itself. Because of the stream issue, this is not the case and the middleware is not stateless. This is not actually a very serious bug, and it would only become an issue if you tried to call the handle() method more than once (see PHPFastCGI). I should say that the final and preferred solution suggested by Anthony Ferrara does not suffer from this bug and is, in my opinion, the way forward.

The bug is more serious in the post by Woody Gilk. Here he provides an example of some middleware that is designed to handle exceptions:

class ExceptionHandler
{
    public function __invoke(
        ServerRequestInterface $request,
        ResponseInterface $response,
        callable $next
    ) {
        try {
            return $next($request, $response);
        } catch (Exception $e) {
            return $this->withException($response, $e);
        }
    }

    private function withException(ResponseInterface $response, Exception $e)
    {
        $response = $response->withStatus(500);
        // ... additional code to write response body
        return $response;
    }
}

In the description, he even says:

The critical thing to note here is that the partial response is never used. If an exception occurs, the response that was passed to this middleware is decorated and returned. So long as your middleware stack has this middleware as close to the top as possible, a badly formatted response will never get decorated as an error.

As we know from Zend Expressive, this is not the case. Anything written to the response body will actually be appended to anything that was written to the response before the exception was thrown. Again, these are not lone mistakes and there are plenty more (1, 2) examples of the immutability assumption bug.