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

Conversation

alihammad-gist
Copy link

  • Are you fixing a bug?
    Yes
    • Detail how the bug is invoked currently.
      By calling the handler that is passed in the middlewares more than once.

    • Detail the original, incorrect behavior.
      The handler Next::$fallbackHandler is invoked more than once. The first invocation
      of $handler->handle inside a middleware drains the middleware queue in Next::queue,
      subsequent invocations of $handler->handle invoke Next::$fallbackHandler.

    • Detail the new, expected behavior.
      The handler should be allowed to be invoked only once or the behavior will be inconsistent. The first middleware
      invokes the second middleware in the pipe when calling hanlder::handle first time,
      second time it will be invoking the handler Next::$fallbackHandler.

    • Base your feature on the master branch, and submit against that branch.

    • Add a regression test that demonstrates the bug, and proves the fix.
      Added the smallest failing test, and the code to pass it in NextTest.php . Didn't remove uneeded Next::$fallbackHandler because one of the tests checks of its existence.

    • Add a CHANGELOG.md entry for the fix.

@Xerkus
Copy link
Member

Xerkus commented Jun 8, 2019

This PR can not be accepted as it breaks intended behavior of the Next handler while not addressing the actual issue of allowing multiple invocations.

The problem you experience is due to your usage. Is there a reason why you need to invoke it multiple times?
Normally middleware acts on request then potentially delegates to next handler then acts on the response returned by next handler.

Do you use this middleware outside of its intended usage as a part of middleware pipe?

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
{
return (new Next($this->pipeline, $handler))->handle($request);
}

I will keep this PR open for your feedback.

@alihammad-gist
Copy link
Author

All test were passing, is there an implicit Next behavior I am ignoring? I added the test, If the test captures a valid Use case instead of an exception please regect the PR. What is the standard behavior when middleware delegates to the next handler twice? This captures the edge case when a continuation is called twice, you are right about normally middle ware delegates to the next handler (once).

public function testCallingContinuationMoreThanOnceRaisesException()
{
$this->expectException(EmptyPipelineException::class);
$fallBackHandler = $this->prophesize(RequestHandlerInterface::class);
$fallBackHandler
->handle(Argument::any())
->shouldBeCalledTimes(1);
// Middleware calling $handler->handle() twice. The first call will empty the
// middleware queue while the second call will raise the empty pipline exception.
$middleware = (new class () implements MiddlewareInterface {
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
$handler->handle($request);
return $handler->handle($request);
}
});
$this->queue->push($middleware);
$next = new Next($this->queue, $fallBackHandler->reveal());
$next->handle($this->request);
}

@Xerkus
Copy link
Member

Xerkus commented Jun 9, 2019

Hm. Next does not provide a way to push to queue once it has been passed to constructor, so one use case I had in mind is out.
That also means that only questionable edge case behavior is affected by pushing final handler to queue. I was too fast to reject it.

Going back to original issue, we have two options here: go ahead and explicitly forbid second invocation or allow multiple invocations.

Second option is easy to achieve by passing cloned Next handler to queued middlewares. Next handler is a hot path though and it will have a performance impact as it will have to be cloned for each piped middleware.

$next = clone $this;
$middleware = $next->queue->dequeue();
return $middleware->process($request, $next);

public function __clone()
{
    $this->queue = clone $this->queue;
}

So, my question stands: Is there a reason why you need to invoke it multiple times?

@weierophinney I would also like your input on the two options outlined above.

@alihammad-gist
Copy link
Author

So, my question stands: Is there a reason why you need to invoke it multiple times?

I do not want to invoke it multiple times, I rather have it throw an exception if it is invoked more than once so I am told explicitly that I have done something wrong.

@Xerkus
Copy link
Member

Xerkus commented Jun 10, 2019

After discussion in slack we came to conclusion that Next handler should not be allowed to be invoked multiple times from the same context as it constitutes bad design and leads to unpredictable and/or hard to test and debug results.

Copy link
Member

@Xerkus Xerkus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summarised the feedback from the slack discussion plus some of the changes i would like to see

src/Next.php Outdated Show resolved Hide resolved
src/Next.php Show resolved Hide resolved
src/Next.php Outdated Show resolved Hide resolved
src/Next.php Show resolved Hide resolved
test/NextTest.php Outdated Show resolved Hide resolved
Copy link
Member

@Xerkus Xerkus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update typehint for Next::$queue property to null|SplQueue

Looking good. Here is final set of changes I would like you to do.

test/NextTest.php Outdated Show resolved Hide resolved
test/NextTest.php Outdated Show resolved Hide resolved
test/NextTest.php Show resolved Hide resolved
test/NextTest.php Outdated Show resolved Hide resolved
test/NextTest.php Outdated Show resolved Hide resolved
test/NextTest.php Outdated Show resolved Hide resolved
test/NextTest.php Show resolved Hide resolved
test/NextTest.php Outdated Show resolved Hide resolved
test/NextTest.php Show resolved Hide resolved
@Xerkus Xerkus changed the base branch from master to develop June 11, 2019 20:25
alihmm and others added 11 commits June 12, 2019 08:00
…ed tests to check repeated invocations in different scenarios
…eue, using DomainException as base for MiddlewarePipeNextHandlerAlreadyCalledException
…d innovations for clarity, also added test for non-specific case of repeated handler invocation by a middleware
The tests cover cases of repeated invocation of Next::handle
by middlewares. Tests are split according to the three mutually
exclusive states of middleware queue Next::$queue, 1 - queue is empty. 2 -
queue contains shortcircuiting middleware, 3 - queue only contains
delegating middlewares.
@Xerkus Xerkus force-pushed the fix/handler-called-more-than-once branch from 8adca0c to 0bc9381 Compare June 11, 2019 22:06
@Xerkus Xerkus added this to the 3.2.0 milestone Jun 11, 2019
This patch elaborates on the details previously in CHANGELOG.md covering
patch zendframework#186, providing an example of _why_ the patch was needed, and
fully detailing the new behavior.
test/NextTest.php Outdated Show resolved Hide resolved
…l statements

Ensures that the expected exception occurs _when_ we expect it.
@weierophinney weierophinney merged commit e227feb into zendframework:develop Jun 12, 2019
@weierophinney
Copy link
Member

Thanks, @alihammad-gist!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants