Skip to content

Commit da623f3

Browse files
committed
Fix concurrent next request handlers and recurse next request handlers
1 parent dae024f commit da623f3

2 files changed

Lines changed: 118 additions & 27 deletions

File tree

src/Io/MiddlewareRunner.php

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@ final class MiddlewareRunner
1616
{
1717
/**
1818
* @var callable[]
19-
* @internal
2019
*/
21-
public $middleware = array();
20+
private $middleware = array();
2221

2322
/**
2423
* @param callable[] $middleware
@@ -38,34 +37,26 @@ public function __invoke(ServerRequestInterface $request)
3837
return Promise\reject(new \RuntimeException('No middleware to run'));
3938
}
4039

41-
$position = 0;
40+
return $this->call($request, 0);
41+
}
4242

43+
/** @internal */
44+
public function call(ServerRequestInterface $request, $position)
45+
{
4346
$that = $this;
44-
$func = function (ServerRequestInterface $request) use (&$func, &$position, &$that) {
45-
$middleware = $that->middleware[$position];
46-
$response = null;
47-
$promise = new Promise\Promise(function ($resolve) use ($middleware, $request, $func, &$response, &$position) {
48-
$position++;
49-
50-
$response = $middleware(
51-
$request,
52-
$func
53-
);
54-
55-
$resolve($response);
56-
}, function () use (&$response) {
57-
if ($response instanceof Promise\CancellablePromiseInterface) {
58-
$response->cancel();
59-
}
60-
});
61-
62-
return $promise->then(null, function ($error) use (&$position) {
63-
$position--;
64-
65-
return Promise\reject($error);
66-
});
47+
$next = function (ServerRequestInterface $request) use ($that, $position) {
48+
return $that->call($request, $position + 1);
6749
};
6850

69-
return $func($request);
51+
$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+
}
7061
}
7162
}

tests/Io/MiddlewareRunnerTest.php

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
use React\Tests\Http\Middleware\ProcessStack;
1313
use React\Tests\Http\TestCase;
1414
use RingCentral\Psr7\Response;
15+
use Psr\Http\Message\RequestInterface;
16+
use React\Promise\CancellablePromiseInterface;
1517

1618
final class MiddlewareRunnerTest extends TestCase
1719
{
@@ -28,6 +30,39 @@ public function testDefaultResponse()
2830
Block\await($middlewareStack($request), Factory::create());
2931
}
3032

33+
public function testReturnsRejectedPromiseIfHandlerThrowsException()
34+
{
35+
$middleware = new MiddlewareRunner(array(
36+
function (ServerRequestInterface $request) {
37+
throw new \RuntimeException('hello');
38+
}
39+
));
40+
41+
$request = new ServerRequest('GET', 'http://example.com/');
42+
43+
$promise = $middleware($request);
44+
45+
$promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException')));
46+
}
47+
48+
/**
49+
* @requires PHP 7
50+
*/
51+
public function testReturnsRejectedPromiseIfHandlerThrowsThrowable()
52+
{
53+
$middleware = new MiddlewareRunner(array(
54+
function (ServerRequestInterface $request) {
55+
throw new \Error('hello');
56+
}
57+
));
58+
59+
$request = new ServerRequest('GET', 'http://example.com/');
60+
61+
$promise = $middleware($request);
62+
63+
$promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf('Throwable')));
64+
}
65+
3166
public function provideProcessStackMiddlewares()
3267
{
3368
$processStackA = new ProcessStack();
@@ -342,4 +377,69 @@ public function testUncommonMiddlewareArrayFormats($middlewareFactory, $expected
342377

343378
$this->assertSame($expectedSequence, (string) $response->getBody());
344379
}
380+
381+
public function testPendingNextRequestHandlersCanBeCalledConcurrently()
382+
{
383+
$called = 0;
384+
$middleware = new MiddlewareRunner(array(
385+
function (RequestInterface $request, $next) {
386+
$first = $next($request);
387+
$second = $next($request);
388+
389+
return new Response();
390+
},
391+
function (RequestInterface $request) use (&$called) {
392+
++$called;
393+
394+
return new Promise\Promise(function () { });
395+
}
396+
));
397+
398+
$request = new ServerRequest('GET', 'http://example.com/');
399+
400+
$response = Block\await($middleware($request), Factory::create());
401+
402+
$this->assertTrue($response instanceof ResponseInterface);
403+
$this->assertEquals(2, $called);
404+
}
405+
406+
public function testCancelPendingNextHandler()
407+
{
408+
$once = $this->expectCallableOnce();
409+
$middleware = new MiddlewareRunner(array(
410+
function (RequestInterface $request, $next) {
411+
$ret = $next($request);
412+
$ret->cancel();
413+
414+
return $ret;
415+
},
416+
function (RequestInterface $request) use ($once) {
417+
return new Promise\Promise(function () { }, $once);
418+
}
419+
));
420+
421+
$request = new ServerRequest('GET', 'http://example.com/');
422+
423+
$middleware($request);
424+
}
425+
426+
public function testCancelResultingPromiseWillCancelPendingNextHandler()
427+
{
428+
$once = $this->expectCallableOnce();
429+
$middleware = new MiddlewareRunner(array(
430+
function (RequestInterface $request, $next) {
431+
return $next($request);
432+
},
433+
function (RequestInterface $request) use ($once) {
434+
return new Promise\Promise(function () { }, $once);
435+
}
436+
));
437+
438+
$request = new ServerRequest('GET', 'http://example.com/');
439+
440+
$promise = $middleware($request);
441+
442+
$this->assertTrue($promise instanceof CancellablePromiseInterface);
443+
$promise->cancel();
444+
}
345445
}

0 commit comments

Comments
 (0)