What is middleware?

What exactly is middleware? In real application when the request comes to the server it has to go through the different request handlers. For example, it could be authentication, validation, ACL, logging, caching and so on. Consider the request-response circle as an onion and when a request comes in, it has to go through the different layers of this onion, to get to the core. And every middleware is a layer of the onion. It is a callable object that receives the request and can modify it (or modify the response) before passing it to the next middleware in the chain (to the next layer of the onion).

middleware

Defining middleware

Let’s start with a simple server example:

<?php 

use React\Http\Server;
use React\Http\Response;
use React\EventLoop\Factory;
use Psr\Http\Message\ServerRequestInterface;

$loop = Factory::create();

$server = new Server(function (ServerRequestInterface $request) {
    return new Response(200, ['Content-Type' => 'text/plain'],  "Hello world\n");
});

$socket = new \React\Socket\Server('127.0.0.1:8000', $loop);
$server->listen($socket);

echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . "\n";

$loop->run();

This code represents a dummy server, that returns Hello world responses to all incoming requests. But for our needs it is OK. Now, what if we want to log all incoming requests? So, let’s add a line with echo:

<?php

$server = new Server(function (ServerRequestInterface $request) {
    echo date('Y-m-d H:i:s') . ' ' . $request->getMethod() . ' ' . $request->getUri() . PHP_EOL;
    return new Response(200, ['Content-Type' => 'text/plain'],  "Hello world\n");
});

When we run our server and make a request to it (I use Curl in terminal) we see a log output on the server console:

http-server-logging

And now we can extract this logging logic into the logging middleware. ReactPHP middleware:

  • is a callable
  • accepts ServerRequestInterface as the first argument and optional callable as the second one
  • returns a ResponseInterface (or any promise which can be consumed by Promise\resolve resolving to a ResponseInterface)
  • calls $next($request) to continue chaining to the next middleware or returns explicitly to abort the chain

So, following these rules a logging middleware function will look like this:

<?php

$loggingMiddleware = function(ServerRequestInterface $request, callable $next) {
    echo date('Y-m-d H:i:s') . ' ' . $request->getMethod() . ' ' . $request->getUri() . PHP_EOL;
    return $next($request);
}

The server constructor can accept an array of callables, where we can pass our middleware:

<?php

$loggingMiddleware = function(ServerRequestInterface $request, callable $next) {
    echo date('Y-m-d H:i:s') . ' ' . $request->getMethod() . ' ' . $request->getUri() . PHP_EOL;
    return $next($request);
};

$server = new Server([
    $loggingMiddleware,
    function (ServerRequestInterface $request) {
        return new Response(200, ['Content-Type' => 'text/plain'],  "Hello world\n");
    }
]);

$socket = new \React\Socket\Server('127.0.0.1:8000', $loop);
$server->listen($socket);

echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . "\n";

$loop->run();

This code does the same logging. When the request comes in our first $loggingMiddleware is executed. It prints out a log message to the server console and then passes a request object to the next middleware which returns a response and ends the chain. This is a very simple example and doesn’t show the real power of middleware when you have some complicated logic, where you modify request and response objects during the request-response life-cycle.

Attaching middleware to a server

For better understanding middleware we can use a simple video streaming server from one of the previous articles. Here is the source code for it:

<?php

$server = new Server(function (ServerRequestInterface $request) use ($loop) {
    $params = $request->getQueryParams();
    $file = $params['video'] ?? '';

    if (empty($file)) {
        return new Response(200, ['Content-Type' => 'text/plain'], 'Video streaming server');
    }

    $filePath = __DIR__ . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . basename($file);
    @$fileStream = fopen($filePath, 'r');
    if (!$fileStream) {
        return new Response(404, ['Content-Type' => 'text/plain'], "Video $file doesn't exist on server.");
    }

    $video = new \React\Stream\ReadableResourceStream($fileStream, $loop);

    return new Response(200, ['Content-Type' => getMimeTypeByExtension($filePath)], $video);
});

$socket = new \React\Socket\Server('127.0.0.1:8000', $loop);
$server->listen($socket);

echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . "\n";
$loop->run();

How does it work? When you open your browser on URL 127.0.0.1:8000 and don’t provide any query params the server returns a blank page with Video streaming server message. To open a video in the browser you can specify video query param like this: http://127.0.0.1:8000/?video=bunny.mpg. If there is a file called bunny.mpg in server media directory, the server starts streaming this file. Very simple.

Note that this example uses fopen() for simplicity and demo purposes only! This should not be used in a truly asynchronous application because the filesystem is inherently blocking and each call could potentially take several seconds. Read this tutorial in case you need to work asynchronously with the filesystem in ReactPHP ecosystem.

getMimeTypeByExtension() is a custom function to detect file MIME type by its extension. You can find its implementation in Video streaming server article.

You can notice that this request handling logic can be separated into three parts:

  • a plain text response, when there is no video query param.
  • 404 response, when a requested file is not found.
  • a streaming response.

These three parts are good candidates for middleware. Let’s start with the first one: $queryParamMiddleware. It simply checks query params. If video param is present it passes the request to the next middleware, otherwise, it stops the chain and returns a plain text response:

<?php

$queryParamMiddleware = function(ServerRequestInterface $request, callable $next) {
    $params = $request->getQueryParams();

    if (!isset($params['video'])) {
        return new Response(200, ['Content-Type' => 'text/plain'], 'Video streaming server');
    }
    
    return $next($request);
};

http-middleware-server-welcome-message

Then, if the request has reached the second middleware, that means that we have video query param. So, we can check if a specified file exists on the server. If not we return 404 response, otherwise we continue chaining to the next middleware:

<?php

$checkFileExistsMiddleware = function(ServerRequestInterface $request, callable $next) {
    $file = $request->getQueryParams()['video'];
    $filePath = __DIR__ . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . basename($file);
    @$fileStream = fopen($filePath, 'r');

    if (!$fileStream) {
        return new Response(404, ['Content-Type' => 'text/plain'], "Video $file doesn't exist on server.");
    }
    
    return $next($request);
};

I’m using fopen here to check if a file exists. file_exists() call is blocking and may lead to race conditions, so it is not recommended to use it in an asynchronous application.

http-middleware-file-not-found

And the last third middleware opens a stream, wraps it into ReactPHP \React\Stream\ReadableResourceStream object and returns it as a response body. This middleware doesn’t accept $next argument because it is the last middleware in our chain. But, notice that it uses an event loop to create \React\Stream\ReadableResourceStream object:

<?php

$videoStreamingMiddleware = function(ServerRequestInterface $request) use ($loop) {
    $file = $request->getQueryParams()['video'];
    $filePath = __DIR__ . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . basename($file);

    $video = new \React\Stream\ReadableResourceStream(fopen($filePath, 'r'), $loop);
    return new Response(200, ['Content-Type' => getMimeTypeByExtension($filePath)], $video);
};

Now, having all these three middleware we can provide them to the Server constructor as an array:

<?php

$server = new Server([
    $queryParamMiddleware,
    $checkFileExistsMiddleware,
    $videoStreamingMiddleware
]);

simple streaming server

The code looks much cleaner than having all this request handling logic in one callback. Our request-response cycle consists of three layers of the middleware onion:

  • $queryParamMiddleware
  • $checkFileExistsMiddleware
  • $videoStreamingMiddleware

When the request comes in it has to go through all these layers. And each layer decides whether to continue chaining or we are done and a response should be returned.

When middleware becomes too complicated it can be extracted to its own class, that implements magic __invoke() method. This allows customizing middleware on the fly.

Modifying response

PHP community has already standardized middleware under PSR-15: HTTP message interfaces, but ReactPHP doesn’t provide any interfaces for middleware implementations. So, don’t confuse PSR-15 middleware and ReactPHP HTTP middleware. As you can notice ReactPHP middleware doesn’t accept the response object, but only request:

<?php

$myMiddleware = function (ServerRequestInterface $request, callable $next) {
    // ...
}

So, it may look like there is no way to modify the response. But it is not exactly the truth. It may look a little tricky, but you can. Let me show how.

In this example we are going to add some headers to the response. We create a server with an array of two middleware: the first one is going to add a custom header to the resulting response, and the second one simply returns the response:

<?php

$server = new Server([
    function (ServerRequestInterface $request, callable $next) {
        // add custom header
    },
    function (ServerRequestInterface $request) {
         return new Response(200, ['Content-Type' => 'text/plain'],  "Hello world\n");
    }
]);

So, how can we modify the response returned by the next middleware? We know that the $next variable represents the next middleware in the chain, so we can explicitly call it and pass a request object to it:

<?php

$server = new Server([
    function (ServerRequestInterface $request, callable $next) {
        return $next($request);
    },
    function (ServerRequestInterface $request) {
         return new Response(200, ['Content-Type' => 'text/plain'],  "Hello world\n");
    }
]);

In this snippet, the first middleware simply continues the chain and returns the response from the next middleware. We can assign the Response from the next middleware to a variable, modify it and then return a modified response:

<?php

$server = new Server([
    function (ServerRequestInterface $request, callable $next) {
        $response = $next($request);
        return $response->withHeader('X-Custom', 'foo');
    },
    function (ServerRequestInterface $request) {
        return new Response(200, ['Content-Type' => 'text/plain'],  "Hello world\n");
    }
]);

And now we can add our custom X-Custom header with foo value and check if everything works as expected:

http-middleware-curl-custom-header

I use Curl in terminal with -i flag to receive the response with headers. You can see that the server returns a response from the second middleware with Hello world message. And also response headers contain our X-Custom header.

Middleware under the hood of the Server

ReactPHP HTTP Component comes with three middleware implementations:

  • LimitConcurrentRequestsMiddleware
  • RequestBodyParserMiddleware
  • RequestBodyBufferMiddleware

All of them under the hood are included in Server class, so there is no need to explicitly pass them. Why these particular middleware? Because they are required to match PHP’s request behavior.

Limiting concurrent requests

LimitConcurrentRequestsMiddleware can be used to limit how many next handlers can be executed concurrently. Server class tries to determine this number automatically according to your php.ini settings (it uses memory_limit and post_max_size values). But a predefined maximum number of pending handlers is 100. This middleware has its own queue. If the number of pending handlers exceeds the allowed limit, the request goes to the queue and its streaming body is paused. Once one of the pending requests is done, the middleware fetches the oldest pending request from the queue and resumes its streaming body.

limit-concurrent-requests

To demonstrate how it works, we can attach a timer for 2 seconds in one of the middleware to simulate a busy server:

<?php

$server = new Server([
    function(ServerRequestInterface $request, callable $next) use ($loop) {
        $deferred = new \React\Promise\Deferred();
        $loop->addTimer(2, function() use ($next, $request, $deferred) {
            echo 'Resolving request' . PHP_EOL;
            $deferred->resolve($next($request));
        });

        return $deferred->promise();
    },
    function (ServerRequestInterface $request) {
        return new Response(200, ['Content-Type' => 'text/plain'],  "Hello world\n");
    }
]);

Then when running two parallel Curl requests we can see that they both are resolved with a delay of 2 seconds:

http-middleware-limit-concurrent-requests-middleware

And now see what happens if we use LimitConcurrentRequestsMiddleware and set the limit to 1:

<?php

$server = new Server([
    new \React\Http\Middleware\LimitConcurrentRequestsMiddleware(1),
    function(ServerRequestInterface $request, callable $next) use ($loop) {
        $deferred = new \React\Promise\Deferred();
        $loop->addTimer(2, function() use ($next, $request, $deferred) {
            echo 'Resolving request' . PHP_EOL;
            $deferred->resolve($next($request));
        });

        return $deferred->promise();
    },
    function (ServerRequestInterface $request) {
        return new Response(200, ['Content-Type' => 'text/plain'],  "Hello world\n");
    }
]);

limit-concurrent-requests-middleware-queue

The requests are queued. While the first request is being processed, the second one is stored in the middleware’s queue. After two seconds, when the first request is done, the second one is dispatched from the queue and then processed. In this way, we have actually removed concurrency and incoming requests are processed by server simply one by one.

Buffering request body

When POST or PUT request reaches HTTP server we can get access to its body by calling $request->getParsedBody(). This method returns an associative array that represents a parsed request body. Under the hood, the server receives a request which body is a stream. So, behind the scenes, React\Http\Server at first uses RequestBodyBufferMiddleware to buffer this stream in memory. The request is buffered until its body end has been reached and then the next middleware in the chain will be called with a complete, buffered request. And the next middleware is RequestBodyParserMiddleware.

Parsing request body

When a request body is buffered it goes to RequestBodyParserMiddleware which actually parses form fields and file uploads. This middleware makes it possible to receive request params, when you call $request->getParsedBody():

<?php

$server = new \React\Http\Server([
    function (ServerRequestInterface $request) {
        print_r($request->getParsedBody());
        return new Response(200, ['Content-Type' => 'text/plain'],  "Hello world\n");
    }
]);

To check how it works we again use Curl from terminal and provide some form data curl 127.0.0.1:8000 --data "param1=value1&param2=value2":

parse-requests-body-middleware

Under the hood this middleware parses requests that use Content-Type: application/x-www-form-urlencoded or Content-Type: multipart/form-data headers. That allows us to get access to both request body and uploads.

Uploading files

To get an instance of the uploaded file you can $request->getUploadedFiles(). This method returns an array of Psr\Http\Message\UploadedFileInterface instances. Each upload from the submitted request is represented by its own instance:

<?php

$server = new \React\Http\Server([
    function (ServerRequestInterface $request) {
        $files = $request->getUploadedFiles();

        /** @var \Psr\Http\Message\UploadedFileInterface|null $file */
        $file = $files['file'] ?? null;

        return new Response(
            200, 
            ['Content-Type' => 'text/plain'],  
            $file ? $file->getClientFilename() : ''
        );
    }
]);

For example, we can upload a simple text file from terminal curl 127.0.0.1:8000 -F "file=@text.txt" (flag -F means multipart/form-data):

parse-requests-body-upload-middleware

This code simply returns an uploaded file name as a response.

To store an uploaded file use UploadedFileInterface::getStream() method, which returns an instance of the Psr\Http\Message\StreamInterface. To get the contents of the uploaded file we can cast this object to a string. For example, this snippet opens a writable stream and stores the contents of the uploaded file in dest.txt:

<?php 

$server = new \React\Http\Server([
    function (ServerRequestInterface $request) use ($loop) {
        /** @var \Psr\Http\Message\UploadedFileInterface[] $files */
        $files = $request->getUploadedFiles();
        $file = $files['file'] ?? null;

        if($file) {
            $dest = new \React\Stream\WritableResourceStream(fopen('dest.txt', 'w'), $loop);
            $dest->write($file->getStream());
        }

        return new Response(200, ['Content-Type' => 'text/plain']);
    }
]);

Actually, UploadedFileInterface has moveTo() method that can be used exactly to store an upload. But this method by its nature is going to be blocking (as a recommended alternative to calling move_uploaded_file()), so it can’t be used in an asynchronous application. That is the reason why we have to use getStream(). Calling moveTo() will throw RuntimeException.

Conclusion

A chain of middleware between the server and your application is a powerful tool to customize the way how your request and response behave. In addition to already built-in Server middleware, there is a list of third-party middleware created by ReactPHP community.


You can find examples from this article on GitHub.

This article is a part of the ReactPHP Series.

Learning Event-Driven PHP With ReactPHP

The book about asynchronous PHP that you NEED!

A complete guide to writing asynchronous applications with ReactPHP. Discover event-driven architecture and non-blocking I/O with PHP!

Review by Pascal MARTIN

Minimum price: 5.99$