Serve PSR-7 Middleware Via React

I've been intending to play with React for some time, but, for one reason or another, kept putting it off. This past week, I carved some time finally to experiment with it, and, specifically, to determine if serving PSR-7 middleware was possible.

React

For those of you unfamiliar with it, React is a project with the goal of providing event-driven, asynchronous PHP, in a vein similar to node.js. To accomplish this, it makes use of one of several experimental extensions, falling back to PHP's built-in tick() support and stream utilities. The project provides the event loop implementation; a Promises library; a cross-platform, low-level socket library; an HTTP server; and several other libraries.

The library that most associate with React, though, is the HTTP server. This library is in the same vein as node's HTTP module, which provides the low-level plumbing for creating HTTP servers.

A basic server looks like this:

$app = function ($request, $response) {
    $response->writeHead(200, array('Content-Type' => 'text/plain'));
    $response->end("Hello World\n");
};

$loop = React\EventLoop\Factory::create();
$socket = new React\Socket\Server($loop);
$http = new React\Http\Server($socket, $loop);

$http->on('request', $app);
echo "Server running at http://127.0.0.1:1337\n";

$socket->listen(1337);
$loop->run();

What I wanted to do was get React to execute PSR-7 middleware, specifically an Expressive application.

Translating React to PSR-7

React provides its own request and response implementations. As noted, these are very much aligned with node, down to the level that each is a stream, with additional methods based on the message type. If you've played with node at all, React's HTTP layer will feel very familiar.

The problem, though, is that the messages differ from PSR-7; you can't just pass them into middleware expecting PSR-7 and have everything work. So, a translation layer was required.

This boiled down to two tasks:

  • Marshaling a PSR-7 request instance from the React request.
  • Pulling information from the PSR-7 response returned by middleware, and using that information to populate and write to the React response.

I discovered quickly that the latest stable release and the current master branch of react/http differ significantly. In particular, the current master branch offers a number of new features, such as URL discovery and file upload handling, which make generating the PSR-7 request far easier. In the end, the logic becomes something like this:

$body = fopen('php://temp', 'w+');
fwrite($body, $reactRequest->getBody());
fseek($body, 0); // Rewind the stream

return new Zend\Diactoros\ServerRequest(
    $_SERVER,
    $reactRequest->getFiles(),
    $reactRequest->getUrl(),
    $reactRequest->getMethod(),
    $body,
    $reactRequest->getHeaders(),
    [], // cookies; these can be handled by PSR-7 middleware
    $reactRequest->getQuery(),
    $reactRequest->getPost(),
    $reactRequest->getHttpVersion()
);

Handling the response can be relatively simple:

$reactResponse->writeHead(
    $psr7Response->getStatusCode(),
    $psr7Response->getHeaders()
);
$reactResponse->end((string) $psr7Response->getBody())

That said, I found it was useful to perform a few additional things:

  • If no content type is set, set it to text/html.
  • Rewind the PSR-7 response body before retrieving it. I've occasionally observed truncated content otherwise.
  • Close the PSR-7 response body when done. This will close the underlying resource, freeing up memory.
if (! $psr7Response->hasHeader('Content-Type')) {
    $psr7Response = $psr7Response->withHeader('Content-Type', 'text/html');
}

$reactResponse->writeHead(
    $psr7Response->getStatusCode(),
    $psr7Response->getHeaders()
);

$body = $psr7Response->getBody();
$body->rewind();

$reactResponse->end($body->getContents());
$body->close();

I also at one point attempted to iterate through the PSR-7 stream, like this:

while (! $body->eof()) {
    $reactResponse->write($body->read(4096));
}
$reactResponse->end();

Unfortunately, this never worked, and led to connection timeouts. If somebody in the React community wants to edify my as to why (or how I could make it work), I'd appreciate it!

Static files

When you create an HTTP server, it's often useful to serve static files: CSS, JS, images, etc. Out of the box, however, React does not do so.

I tried an approach using React's filesystem library, but had no luck with it; for some reason, file contents were never returned. As such, I took another approach entirely, and wrote PSR-7 middleware to serve the files, making this the outer layer of my middleware so that it executes earliest, and then delegates to the application middleware when files are not found. I also have the middleware:

  • implement a whitelist, to restrict which files may be served
  • match directories to index files (e.g., index.html)
$path = $this->root . $request->getUri()->getPath();
if (is_dir($path)) {
    $path = rtrim($path, '/') . '/index.html';
}

if (! preg_match('#\.(?P<type>[a-z][a-z0-9]{0,3})$#', $path, $matches)) {
    return $next($request, $response);
}

$type = $matches['type'];
if (! in_array($type, array_keys($this->contentTypeMap), true)) {
    return $next($request, $response);
}

if (! file_exists($path)) {
    return $next($request, $response);
}

return $response
    ->withHeader('Content-Type', $this->contentTypeMap[$type])
    ->withBody(new Stream($path, 'r'));

The fun part of this is that, because PSR-7 and React both deal with streams, the approach is incredibly performant, and uses very few resources!

Making it reusable

To make this reusable, I created a new library, phly/react2psr7. This library contains:

  • React2Psr7\ReactRequestHandler, which accepts a PSR-7 middleware to its constructor, and then, for each invocation, marshals a PSR-7 request, creates an empty PSR-7 response, dispatches the middleware, and uses the returned response to feed the React response.
  • React2Psr7\StaticFiles, which is the PSR-7 middleware for serving static files from the filesystem.

Install it using:

$ composer require "react/http:^0.5@dev" phly/react2psr7

(Since this uses the current development series of react/http, you need to install that package manually.)

A basic server script for an Expressive application then looks like this:

<?php
// server.php
use React\EventLoop\Factory;
use React\Http\Server as HttpServer;
use React\Socket\Server as Socket;
use React2Psr7\ReactRequestHandler;
use Zend\Expressive\Application;

require_once 'vendor/autoload.php';

$loop      = Factory::create();
$socket    = new Socket($loop);
$http      = new HttpServer($socket);
$container = require 'config/container.php';

$http->on('request', new ReactRequestHandler($container->get(Application::class)));

// Listen on all ports; omit second argument to restrict to localhost.
$socket->listen(1337, '0.0.0.0');
$loop->run();

For Expressive, I also added configuration for the StaticFiles middleware to my config/autoload/middleware-pipeline.global.php file:

return [
    'dependencies' => [
        'factories' => [
            React2Psr7\StaticFiles::class => React2Psr7\StaticFilesFactory::class,
            /* ... */
        ],
    ],
    'middleware_pipeline' => [
        'static' => [
            'middleware' => React2Psr7\StaticFiles::class,
            'priority' => 100000, // Execute earliest!
        ],
        /* ... */
    ],
];

Fire up the server:

$ php server.php

And then start making requests (the following is using HTTPie):

$ http GET localhost:1337/api/ping
$ http GET localhost:1337/zf-logo.png
$ http GET localhost:1337/

Next steps

I have a couple things on my roadmap still:

  • I need to play with file uploads to see how those are handled, and the impact to performance and resource usage. Right now there's a potential for duplication of the resources, which makes me hesitant to use it in such scenarios.
  • I'd like to try and create a variant of the React HTTP server that marshals PSR-7 requests and responses and emits the PSR-7 response directly, instead of requiring casting. This would largely solve the above problems.
  • Documentation for the React project. Currently, each subproject has a README file that details the simplest use case, but anything more requires diving through the code. In several cases, I determined that methods are often overloaded to return promises, but how and where that happens is not clear. As such, while the basics of the system are fairly easy to pick up, anything more requires a ton of domain knowledge, which makes in unapproachable. I'd love to help solve that problem through documentation.

In the meantime, I'm quite happy with my weekend experiment!