Skip to content

Commit e38a527

Browse files
authored
Merge pull request #294 from clue-labs/happy-middleware
Avoid promise wrapping for middleware next request handlers and add documentation for consuming response from next request handler
2 parents 800ca51 + cfd5d5f commit e38a527

3 files changed

Lines changed: 108 additions & 34 deletions

File tree

README.md

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -798,10 +798,27 @@ As such, this project supports the concept of middleware request handlers.
798798

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

801-
* It is a `callable`.
802-
* It accepts `ServerRequestInterface` as first argument and optional `callable` as second argument.
803-
* It returns a `ResponseInterface` (or any promise which can be consumed by [`Promise\resolve`](http://reactphp.org/promise/#resolve) resolving to a `ResponseInterface`)
804-
* It calls `$next($request)` to continue processing the next middleware function or returns explicitly to abort the chain
801+
* It is a valid `callable`.
802+
* It accepts `ServerRequestInterface` as first argument and an optional
803+
`callable` as second argument.
804+
* It returns either:
805+
* An instance implementing `ResponseInterface` for direct consumption.
806+
* Any promise which can be consumed by
807+
[`Promise\resolve()`](http://reactphp.org/promise/#resolve) resolving to a
808+
`ResponseInterface` for deferred consumption.
809+
* It MAY throw an `Exception` (or return a rejected promise) in order to
810+
signal an error condition and abort the chain.
811+
* It calls `$next($request)` to continue processing the next middleware
812+
request handler function or returns explicitly without calling `$next` to
813+
abort the chain.
814+
* The `$next` request handler function (recursively) invokes the next request
815+
handler from the chain with the same logic as above and returns (or throws)
816+
as above.
817+
* The `$request` may be modified prior to calling `$next($request)` to
818+
change the incoming request the next middleware operates on.
819+
* The `$next` return value may be consumed to modify the outgoing response.
820+
* The `$next` request handler MAY be called more than once if you want to
821+
implement custom "retry" logic etc.
805822

806823
Note that this very simple definition allows you to use either anonymous
807824
functions or any classes that use the magic `__invoke()` method.
@@ -828,9 +845,62 @@ $server = new Server(array(
828845
$request = $request->withHeader('Request-Time', time());
829846
return $next($request);
830847
},
848+
function (ServerRequestInterface $request) {
849+
return new Response(200);
850+
}
851+
));
852+
```
853+
854+
Similarly, you can use the result of the `$next` middleware request handler
855+
function to modify the outgoing response.
856+
Note that as per the above documentation, the `$next` method may return a
857+
`ResponseInterface` directly or one wrapped in a promise for deferred
858+
resolution.
859+
In order to simplify handling both paths, you can simply wrap this in a
860+
[`Promise\resolve()`](http://reactphp.org/promise/#resolve) call like this:
861+
862+
```php
863+
$server = new Server(array(
831864
function (ServerRequestInterface $request, callable $next) {
865+
$promise = React\Promise\resolve($next($request));
866+
return $promise->then(function (ResponseInterface $response) {
867+
return $response->withHeader('Content-Type', 'text/html');
868+
});
869+
},
870+
function (ServerRequestInterface $request) {
832871
return new Response(200);
872+
}
873+
));
874+
```
875+
876+
Note that the `$next` middleware request handler function may also throw an
877+
`Exception` (or return a rejected promise) as described above.
878+
The previous example does not catch any exceptions and would thus signal an
879+
error condition to the `Server`.
880+
Alternatively, you can also catch any `Exception` to implement custom error
881+
handling logic (or logging etc.) by wrapping this in a
882+
[`Promise`](http://reactphp.org/promise/#promise) like this:
883+
884+
```php
885+
$server = new Server(array(
886+
function (ServerRequestInterface $request, callable $next) {
887+
$promise = new React\Promise\Promise(function ($resolve) use ($next, $request) {
888+
$resolve($next($request));
889+
});
890+
return $promise->then(null, function (Exception $e) {
891+
return new Response(
892+
500,
893+
array(),
894+
'Internal error: ' . $e->getMessage()
895+
);
896+
});
833897
},
898+
function (ServerRequestInterface $request) {
899+
if (mt_rand(0, 1) === 1) {
900+
throw new RuntimeException('Database error');
901+
}
902+
return new Response(200);
903+
}
834904
));
835905
```
836906

src/Io/MiddlewareRunner.php

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
use Psr\Http\Message\ResponseInterface;
66
use Psr\Http\Message\ServerRequestInterface;
7-
use React\Promise;
87
use React\Promise\PromiseInterface;
98

109
/**
@@ -29,12 +28,13 @@ public function __construct(array $middleware)
2928

3029
/**
3130
* @param ServerRequestInterface $request
32-
* @return PromiseInterface<ResponseInterface>
31+
* @return ResponseInterface|PromiseInterface<ResponseInterface>
32+
* @throws Exception
3333
*/
3434
public function __invoke(ServerRequestInterface $request)
3535
{
3636
if (count($this->middleware) === 0) {
37-
return Promise\reject(new \RuntimeException('No middleware to run'));
37+
throw new \RuntimeException('No middleware to run');
3838
}
3939

4040
return $this->call($request, 0);
@@ -49,14 +49,6 @@ public function call(ServerRequestInterface $request, $position)
4949
};
5050

5151
$handler = $this->middleware[$position];
52-
try {
53-
return Promise\resolve($handler($request, $next));
54-
} catch (\Exception $error) {
55-
// request handler callback throws an Exception
56-
return Promise\reject($error);
57-
} catch (\Throwable $error) { // @codeCoverageIgnoreStart
58-
// request handler callback throws a PHP7+ Error
59-
return Promise\reject($error); // @codeCoverageIgnoreEnd
60-
}
52+
return $handler($request, $next);
6153
}
6254
}

tests/Io/MiddlewareRunnerTest.php

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,28 @@
1414
use RingCentral\Psr7\Response;
1515
use Psr\Http\Message\RequestInterface;
1616
use React\Promise\CancellablePromiseInterface;
17+
use React\Promise\PromiseInterface;
1718

1819
final class MiddlewareRunnerTest extends TestCase
1920
{
2021
/**
2122
* @expectedException RuntimeException
2223
* @expectedExceptionMessage No middleware to run
2324
*/
24-
public function testDefaultResponse()
25+
public function testEmptyMiddlewareStackThrowsException()
2526
{
2627
$request = new ServerRequest('GET', 'https://example.com/');
2728
$middlewares = array();
2829
$middlewareStack = new MiddlewareRunner($middlewares);
2930

30-
Block\await($middlewareStack($request), Factory::create());
31+
$middlewareStack($request);
3132
}
3233

33-
public function testReturnsRejectedPromiseIfHandlerThrowsException()
34+
/**
35+
* @expectedException RuntimeException
36+
* @expectedExceptionMessage hello
37+
*/
38+
public function testThrowsIfHandlerThrowsException()
3439
{
3540
$middleware = new MiddlewareRunner(array(
3641
function (ServerRequestInterface $request) {
@@ -40,15 +45,15 @@ function (ServerRequestInterface $request) {
4045

4146
$request = new ServerRequest('GET', 'http://example.com/');
4247

43-
$promise = $middleware($request);
44-
45-
$promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException')));
48+
$middleware($request);
4649
}
4750

4851
/**
4952
* @requires PHP 7
53+
* @expectedException Throwable
54+
* @expectedExceptionMessage hello
5055
*/
51-
public function testReturnsRejectedPromiseIfHandlerThrowsThrowable()
56+
public function testThrowsIfHandlerThrowsThrowable()
5257
{
5358
$middleware = new MiddlewareRunner(array(
5459
function (ServerRequestInterface $request) {
@@ -58,9 +63,7 @@ function (ServerRequestInterface $request) {
5863

5964
$request = new ServerRequest('GET', 'http://example.com/');
6065

61-
$promise = $middleware($request);
62-
63-
$promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf('Throwable')));
66+
$middleware($request);
6467
}
6568

6669
public function provideProcessStackMiddlewares()
@@ -126,9 +129,14 @@ public function testProcessStack(array $middlewares, $expectedCallCount)
126129
$request = new ServerRequest('GET', 'https://example.com/');
127130
$middlewareStack = new MiddlewareRunner($middlewares);
128131

129-
/** @var ResponseInterface $result */
130-
$result = Block\await($middlewareStack($request), Factory::create());
131-
$this->assertSame(200, $result->getStatusCode());
132+
$response = $middlewareStack($request);
133+
134+
$this->assertTrue($response instanceof PromiseInterface);
135+
$response = Block\await($response, Factory::create());
136+
137+
$this->assertTrue($response instanceof ResponseInterface);
138+
$this->assertSame(200, $response->getStatusCode());
139+
132140
foreach ($middlewares as $middleware) {
133141
if (!($middleware instanceof ProcessStack)) {
134142
continue;
@@ -163,7 +171,11 @@ public function testNextCanBeRunMoreThanOnceWithoutCorruptingTheMiddlewareStack(
163171
$retryCalled = 0;
164172
$error = null;
165173
$retry = function ($request, $next) use (&$error, &$retryCalled) {
166-
return $next($request)->then(null, function ($et) use (&$error, $request, $next, &$retryCalled) {
174+
$promise = new \React\Promise\Promise(function ($resolve) use ($request, $next) {
175+
$resolve($next($request));
176+
});
177+
178+
return $promise->then(null, function ($et) use (&$error, $request, $next, &$retryCalled) {
167179
$retryCalled++;
168180
$error = $et;
169181
// the $next failed. discard $error and retry once again:
@@ -214,7 +226,7 @@ function (ServerRequestInterface $request, $next) use (&$receivedRequests) {
214226
},
215227
function (ServerRequestInterface $request, $next) use (&$receivedRequests) {
216228
$receivedRequests[] = 'middleware3: ' . $request->getUri();
217-
return $next($request);
229+
return new \React\Promise\Promise(function () { });
218230
}
219231
));
220232

@@ -372,9 +384,9 @@ public function testUncommonMiddlewareArrayFormats($middlewareFactory, $expected
372384
$request = new ServerRequest('GET', 'https://example.com/');
373385
$middlewareStack = new MiddlewareRunner($middlewareFactory());
374386

375-
/** @var ResponseInterface $response */
376-
$response = Block\await($middlewareStack($request), Factory::create());
387+
$response = $middlewareStack($request);
377388

389+
$this->assertTrue($response instanceof ResponseInterface);
378390
$this->assertSame($expectedSequence, (string) $response->getBody());
379391
}
380392

@@ -397,7 +409,7 @@ function (RequestInterface $request) use (&$called) {
397409

398410
$request = new ServerRequest('GET', 'http://example.com/');
399411

400-
$response = Block\await($middleware($request), Factory::create());
412+
$response = $middleware($request);
401413

402414
$this->assertTrue($response instanceof ResponseInterface);
403415
$this->assertEquals(2, $called);

0 commit comments

Comments
 (0)