Skip to content
This repository has been archived by the owner on Jan 29, 2020. It is now read-only.

Throw EmptyPipelineException if passed in continuation is invoked more than once. #186

Merged
Show file tree
Hide file tree
Changes from 11 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
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ details.

### Added

- Nothing.
- [#186](https://github.com/zendframework/zend-stratigility/pull/186) adds safeguard
to middleware pipe Next handler, preventing it from being called multiple
times, which causes undefined behavior after queue of middlewares was
exhausted on the first pass.

### Changed

Expand Down
21 changes: 21 additions & 0 deletions src/Exception/MiddlewarePipeNextHandlerAlreadyCalledException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php
/**
* @see https://github.com/zendframework/zend-stratigility for the canonical source repository
* @copyright Copyright (c) 2019 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://github.com/zendframework/zend-stratigility/blob/master/LICENSE.md New BSD License
*/

declare(strict_types=1);

namespace Zend\Stratigility\Exception;

use DomainException;

class MiddlewarePipeNextHandlerAlreadyCalledException extends DomainException implements ExceptionInterface
{

public static function create(): self
{
return new self("Cannot invoke Next handler more than once");
}
}
14 changes: 11 additions & 3 deletions src/Next.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php
/**
* @see https://github.com/zendframework/zend-stratigility for the canonical source repository
* @copyright Copyright (c) 2015-2018 Zend Technologies USA Inc. (https://www.zend.com)
* @copyright Copyright (c) 2015-2019 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://github.com/zendframework/zend-stratigility/blob/master/LICENSE.md New BSD License
*/

Expand All @@ -13,6 +13,7 @@
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use SplQueue;
use Zend\Stratigility\Exception\MiddlewarePipeNextHandlerAlreadyCalledException;

/**
* Iterate a queue of middlewares and execute them.
Expand All @@ -25,7 +26,7 @@ final class Next implements RequestHandlerInterface
private $fallbackHandler;

/**
* @var SplQueue
* @var null|SplQueue
*/
private $queue;

Expand All @@ -43,12 +44,19 @@ public function __construct(SplQueue $queue, RequestHandlerInterface $fallbackHa

public function handle(ServerRequestInterface $request) : ResponseInterface
{
if ($this->queue === null) {
throw MiddlewarePipeNextHandlerAlreadyCalledException::create();
}

if ($this->queue->isEmpty()) {
$this->queue = null;
return $this->fallbackHandler->handle($request);
}

$middleware = $this->queue->dequeue();
$next = clone $this; // deep clone is not used intentionally
$this->queue = null; // mark queue as processed at this nesting level

return $middleware->process($request, $this);
return $middleware->process($request, $next);
}
}
102 changes: 101 additions & 1 deletion test/NextTest.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php
/**
* @see https://github.com/zendframework/zend-stratigility for the canonical source repository
* @copyright Copyright (c) 2015-2018 Zend Technologies USA Inc. (https://www.zend.com)
* @copyright Copyright (c) 2015-2019 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://github.com/zendframework/zend-stratigility/blob/master/LICENSE.md New BSD License
*/

Expand All @@ -18,9 +18,12 @@
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use SplQueue;
use ZendTest\Stratigility\TestAsset\DelegatingMiddleware;
use ZendTest\Stratigility\TestAsset\ShortCircuitingMiddleware;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequest as Request;
use Zend\Diactoros\Uri;
use Zend\Stratigility\Exception\MiddlewarePipeNextHandlerAlreadyCalledException;
use Zend\Stratigility\Next;

class NextTest extends TestCase
Expand Down Expand Up @@ -214,4 +217,101 @@ public function testMiddlewareReturningResponseShortCircuitsProcess()

$this->assertSame($response, $next->handle($this->request));
}

public function testNextHandlerCannotBeInvokedTwice()
{
$this->expectException(MiddlewarePipeNextHandlerAlreadyCalledException::class);

$fallbackHandler = $this->prophesize(RequestHandlerInterface::class);
$fallbackHandler
->handle(Argument::any())
->willReturn(new Response());

$this->queue->push(new DelegatingMiddleware());

$next = new Next($this->queue, $fallbackHandler->reveal());
$next->handle($this->request);
$next->handle($this->request);
}

public function testSecondInvocationAttemptDoesNotInvokeFinalHandler()
{
$this->expectException(MiddlewarePipeNextHandlerAlreadyCalledException::class);

$fallBackHandler = $this->prophesize(RequestHandlerInterface::class);
$fallBackHandler
->handle(Argument::any())
->willReturn(new Response())
->shouldBeCalledTimes(1);

$this->queue->push(new DelegatingMiddleware());

$next = new Next($this->queue, $fallBackHandler->reveal());
$next->handle($this->request);
$next->handle($this->request);
}

public function testSecondInvocationAttemptDoesNotInvokeMiddleware()
{
$this->expectException(MiddlewarePipeNextHandlerAlreadyCalledException::class);

$fallBackHandler = $this->prophesize(RequestHandlerInterface::class);
$fallBackHandler
->handle(Argument::any())
->willReturn(new Response());

$middleware = $this->prophesize(MiddlewareInterface::class);
$middleware
->process(
Argument::type(ServerRequestInterface::class),
Argument::type(RequestHandlerInterface::class)
)
->will(function (array $args): ResponseInterface {
return $args[1]->handle($args[0]);
})
->shouldBeCalledTimes(1);

$this->queue->push($middleware->reveal());

$next = new Next($this->queue, $fallBackHandler->reveal());
$next->handle($this->request);
$next->handle($this->request);
}

public function testShortCircuitingMiddlewareDoesNotEnableSecondInvocation()
{
$this->expectException(MiddlewarePipeNextHandlerAlreadyCalledException::class);

$fallBackHandler = $this->prophesize(RequestHandlerInterface::class);
$fallBackHandler
->handle(Argument::any())
->willReturn(new Response())
->shouldNotBeCalled();

$this->queue->push(new ShortCircuitingMiddleware());

// The middleware above shorcircuits (when handler is invoked first)
// The middleware below still exists in the queue (when handler is invoked again)
$this->queue->push(new DelegatingMiddleware());

$next = new Next($this->queue, $fallBackHandler->reveal());
$next->handle($this->request);
$next->handle($this->request);
}

public function testSecondInvocationAttemptWithEmptyQueueDoesNotInvokeFinalHandler()
{
$this->expectException(MiddlewarePipeNextHandlerAlreadyCalledException::class);

$fallBackHandler = $this->prophesize(RequestHandlerInterface::class);
$fallBackHandler
->handle(Argument::any())
->willReturn(new Response())
->shouldBeCalledTimes(1);

$next = new Next($this->queue, $fallBackHandler->reveal());

$next->handle($this->request);
$next->handle($this->request);
}
}
23 changes: 23 additions & 0 deletions test/TestAsset/DelegatingMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
/**
* @see https://github.com/zendframework/zend-stratigility for the canonical source repository
* @copyright Copyright (c) 2019 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://github.com/zendframework/zend-stratigility/blob/master/LICENSE.md New BSD License
*/

declare(strict_types=1);

namespace ZendTest\Stratigility\TestAsset;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class DelegatingMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $req, RequestHandlerInterface $handler): ResponseInterface
{
return $handler->handle($req);
}
}
24 changes: 24 additions & 0 deletions test/TestAsset/ShortCircuitingMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php
/**
* @see https://github.com/zendframework/zend-stratigility for the canonical source repository
* @copyright Copyright (c) 2019 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://github.com/zendframework/zend-stratigility/blob/master/LICENSE.md New BSD License
*/

declare(strict_types=1);

namespace ZendTest\Stratigility\TestAsset;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Diactoros\Response;

class ShortCircuitingMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $req, RequestHandlerInterface $handler): ResponseInterface
{
return new Response();
}
}