Skip to content

Simplify using middleware request handlers #277

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 11, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 38 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -659,27 +659,49 @@ passed explicitly.

### Middleware

Middleware can be added to the server using [`MiddlewareRunner`](src/MiddlewareRunner.php)
instead of the `callable`. A middleware is expected to adhere the following rules:
As documented above, the [`StreamingServer`](#streamingserver) accepts a single
request handler argument that is responsible for processing an incoming
HTTP request and then creating and returning an outgoing HTTP response.

Many common use cases involve validating, processing, manipulating the incoming
HTTP request before passing it to the final business logic request handler.
As such, this project supports the concept of middleware request handlers.

A middleware request handler is expected to adhere the following rules:

* It is a `callable`.
* It accepts `ServerRequestInterface` as first argument and optional `callable` as second argument.
* It returns a `ResponseInterface` (or any promise which can be consumed by [`Promise\resolve`](http://reactphp.org/promise/#resolve) resolving to a `ResponseInterface`)
* It calls `$next($request)` to continue processing the next middleware function or returns explicitly to abort the chain

The following example adds a middleware that adds the current time to the request as a
header (`Request-Time`) and middleware that always returns a 200 code without a body:
Note that this very simple definition allows you to use either anonymous
functions or any classes that use the magic `__invoke()` method.
This allows you to easily create custom middleware request handlers on the fly
or use a class based approach to ease using existing middleware implementations.

While this project does provide the means to *use* middleware implementations,
it does not aim to *define* how middleware implementations should look like.
We realize that there's a vivid ecosystem of middleware implementations and
ongoing effort to standardize interfaces between these and support this goal.
As such, this project only bundles a few middleware implementations that are
required to match PHP's request behavior (see below) and otherwise actively
encourages [Third-Party Middleware](#third-party-middleware) implementations.

In order to use middleware request handlers, simply pass an array with all
callables as defined above to the [`StreamingServer`](#streamingserver).
The following example adds a middleware request handler that adds the current time to the request as a
header (`Request-Time`) and a final request handler that always returns a 200 code without a body:

```php
$server = new StreamingServer(new MiddlewareRunner([
$server = new StreamingServer(array(
function (ServerRequestInterface $request, callable $next) {
$request = $request->withHeader('Request-Time', time());
return $next($request);
},
function (ServerRequestInterface $request, callable $next) {
return new Response(200);
},
]));
));
```

#### LimitConcurrentRequestsMiddleware
Expand All @@ -702,37 +724,37 @@ The following example shows how this middleware can be used to ensure no more
than 10 handlers will be invoked at once:

```php
$server = new StreamingServer(new MiddlewareRunner([
$server = new StreamingServer(array(
new LimitConcurrentRequestsMiddleware(10),
$handler
]));
));
```

Similarly, this middleware is often used in combination with the
[`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) (see below)
to limit the total number of requests that can be buffered at once:

```php
$server = new StreamingServer(new MiddlewareRunner([
$server = new StreamingServer(array(
new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers
new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request
new RequestBodyParserMiddleware(),
$handler
]));
));
```

More sophisticated examples include limiting the total number of requests
that can be buffered at once and then ensure the actual request handler only
processes one request after another without any concurrency:

```php
$server = new StreamingServer(new MiddlewareRunner([
$server = new StreamingServer(array(
new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers
new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request
new RequestBodyParserMiddleware(),
new LimitConcurrentRequestsMiddleware(1), // only execute 1 handler (no concurrency)
$handler
]));
));
```

#### RequestBodyBufferMiddleware
Expand Down Expand Up @@ -778,14 +800,14 @@ the total number of concurrent requests.
Usage:

```php
$middlewares = new MiddlewareRunner([
$server = new StreamServer(array(
new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers
new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB
function (ServerRequestInterface $request, callable $next) {
// The body from $request->getBody() is now fully available without the need to stream it
return new Response(200);
},
]);
));
```

#### RequestBodyParserMiddleware
Expand Down Expand Up @@ -837,12 +859,12 @@ $handler = function (ServerRequestInterface $request) {
);
};

$server = new StreamingServer(new MiddlewareRunner([
$server = new StreamingServer(array((
new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers
new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB
new RequestBodyParserMiddleware(),
$handler
]));
));
```

See also [example #12](examples) for more details.
Expand Down
5 changes: 2 additions & 3 deletions examples/12-upload.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UploadedFileInterface;
use React\EventLoop\Factory;
use React\Http\MiddlewareRunner;
use React\Http\Middleware\LimitConcurrentRequestsMiddleware;
use React\Http\Middleware\RequestBodyBufferMiddleware;
use React\Http\Middleware\RequestBodyParserMiddleware;
Expand Down Expand Up @@ -121,12 +120,12 @@
};

// buffer and parse HTTP request body before running our request handler
$server = new StreamingServer(new MiddlewareRunner(array(
$server = new StreamingServer(array(
new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers, queue otherwise
new RequestBodyBufferMiddleware(8 * 1024 * 1024), // 8 MiB max, ignore body otherwise
new RequestBodyParserMiddleware(100 * 1024, 1), // 1 file with 100 KiB max, reject upload otherwise
$handler
)));
));

$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop);
$server->listen($socket);
Expand Down
7 changes: 6 additions & 1 deletion src/MiddlewareRunner.php → src/Io/MiddlewareRunner.php
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
<?php

namespace React\Http;
namespace React\Http\Io;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use React\Promise;
use React\Promise\PromiseInterface;

/**
* [Internal] Middleware runner to expose an array of middleware request handlers as a single request handler callable
*
* @internal
*/
final class MiddlewareRunner
{
/**
Expand Down
14 changes: 7 additions & 7 deletions src/Middleware/LimitConcurrentRequestsMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,37 +27,37 @@
* than 10 handlers will be invoked at once:
*
* ```php
* $server = new StreamingServer(new MiddlewareRunner([
* $server = new StreamingServer(array(
* new LimitConcurrentRequestsMiddleware(10),
* $handler
* ]));
* ));
* ```
*
* Similarly, this middleware is often used in combination with the
* [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) (see below)
* to limit the total number of requests that can be buffered at once:
*
* ```php
* $server = new StreamingServer(new MiddlewareRunner([
* $server = new StreamingServer(array(
* new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers
* new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request
* new RequestBodyParserMiddleware(),
* $handler
* ]));
* $handler
* ));
* ```
*
* More sophisticated examples include limiting the total number of requests
* that can be buffered at once and then ensure the actual request handler only
* processes one request after another without any concurrency:
*
* ```php
* $server = new StreamingServer(new MiddlewareRunner([
* $server = new StreamingServer(array(
* new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers
* new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request
* new RequestBodyParserMiddleware(),
* new LimitConcurrentRequestsMiddleware(1), // only execute 1 handler (no concurrency)
* $handler
* ]));
* ));
* ```
*
* @see RequestBodyBufferMiddleware
Expand Down
11 changes: 7 additions & 4 deletions src/StreamingServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use React\Http\Io\CloseProtectionStream;
use React\Http\Io\HttpBodyStream;
use React\Http\Io\LengthLimitedStream;
use React\Http\Io\MiddlewareRunner;
use React\Http\Io\RequestHeaderParser;
use React\Http\Io\ServerRequest;
use React\Promise\CancellablePromiseInterface;
Expand Down Expand Up @@ -93,16 +94,18 @@ class StreamingServer extends EventEmitter
* connections in order to then parse incoming data as HTTP.
* See also [listen()](#listen) for more details.
*
* @param callable $callback
* @param callable|callable[] $requestHandler
* @see self::listen()
*/
public function __construct($callback)
public function __construct($requestHandler)
{
if (!is_callable($callback)) {
if (is_array($requestHandler)) {
$requestHandler = new MiddlewareRunner($requestHandler);
} elseif (!is_callable($requestHandler)) {
throw new \InvalidArgumentException();
}

$this->callback = $callback;
$this->callback = $requestHandler;
}

/**
Expand Down
37 changes: 5 additions & 32 deletions tests/FunctionalServerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use Psr\Http\Message\ServerRequestInterface;
use React\Http\Middleware\LimitConcurrentRequestsMiddleware;
use React\Http\Middleware\RequestBodyBufferMiddleware;
use React\Http\MiddlewareRunner;
use React\Socket\Server as Socket;
use React\EventLoop\Factory;
use React\Http\StreamingServer;
Expand Down Expand Up @@ -47,42 +46,16 @@ public function testPlainHttpOnRandomPort()
$socket->close();
}

public function testPlainHttpOnRandomPortWithMiddlewareRunner()
public function testPlainHttpOnRandomPortWithSingleRequestHandlerArray()
{
$loop = Factory::create();
$connector = new Connector($loop);

$server = new StreamingServer(new MiddlewareRunner(array(function (RequestInterface $request) {
return new Response(200, array(), (string)$request->getUri());
})));

$socket = new Socket(0, $loop);
$server->listen($socket);

$result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) {
$conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n");

return Stream\buffer($conn);
});

$response = Block\await($result, $loop, 1.0);

$this->assertContains("HTTP/1.0 200 OK", $response);
$this->assertContains('http://' . noScheme($socket->getAddress()) . '/', $response);

$socket->close();
}

public function testPlainHttpOnRandomPortWithEmptyMiddlewareRunner()
{
$loop = Factory::create();
$connector = new Connector($loop);

$server = new StreamingServer(new MiddlewareRunner(array(
$server = new StreamingServer(array(
function () {
return new Response(404);
},
)));
));

$socket = new Socket(0, $loop);
$server->listen($socket);
Expand Down Expand Up @@ -724,7 +697,7 @@ public function testLimitConcurrentRequestsMiddlewareRequestStreamPausing()
$loop = Factory::create();
$connector = new Connector($loop);

$server = new StreamingServer(new MiddlewareRunner(array(
$server = new StreamingServer(array(
new LimitConcurrentRequestsMiddleware(5),
new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB
function (ServerRequestInterface $request, $next) use ($loop) {
Expand All @@ -737,7 +710,7 @@ function (ServerRequestInterface $request, $next) use ($loop) {
function (ServerRequestInterface $request) {
return new Response(200, array(), (string)strlen((string)$request->getBody()));
}
)));
));

$socket = new Socket(0, $loop);
$server->listen($socket);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
<?php

namespace React\Tests\Http;
namespace React\Tests\Http\Io;

use Clue\React\Block;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use React\EventLoop\Factory;
use React\Http\Io\MiddlewareRunner;
use React\Http\Io\ServerRequest;
use React\Http\MiddlewareRunner;
use React\Promise;
use React\Tests\Http\Middleware\ProcessStack;
use React\Tests\Http\TestCase;
use RingCentral\Psr7\Response;

final class MiddlewareRunnerTest extends TestCase
Expand Down
11 changes: 5 additions & 6 deletions tests/StreamingServerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@

namespace React\Tests\Http;

use React\Http\MiddlewareRunner;
use React\Http\StreamingServer;
use Psr\Http\Message\ServerRequestInterface;
use React\Http\Response;
use React\Stream\ThroughStream;
use React\Http\StreamingServer;
use React\Promise\Promise;
use React\Stream\ThroughStream;

class StreamingServerTest extends TestCase
{
Expand Down Expand Up @@ -96,14 +95,14 @@ public function testRequestEvent()
$this->assertSame('127.0.0.1', $serverParams['REMOTE_ADDR']);
}

public function testRequestEventWithMiddlewareRunner()
public function testRequestEventWithSingleRequestHandlerArray()
{
$i = 0;
$requestAssertion = null;
$server = new StreamingServer(new MiddlewareRunner(array(function (ServerRequestInterface $request) use (&$i, &$requestAssertion) {
$server = new StreamingServer(array(function (ServerRequestInterface $request) use (&$i, &$requestAssertion) {
$i++;
$requestAssertion = $request;
})));
}));

$this->connection
->expects($this->any())
Expand Down
2 changes: 1 addition & 1 deletion tests/benchmark-middleware-runner.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<?php

use Psr\Http\Message\ServerRequestInterface;
use React\Http\Io\MiddlewareRunner;
use React\Http\Io\ServerRequest;
use React\Http\MiddlewareRunner;
use React\Http\Response;

const ITERATIONS = 5000;
Expand Down