Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Redirect/Forward/Dispatching AbstractActionController cause the response headers to be sent before the request is completed #4472

Open
akomm opened this Issue · 5 comments

2 participants

Andriy Komm Matthew Weier O'Phinney
Andriy Komm

Dispatching a Dispatchable cause the returned response headers to be sent immediately. See comment in the code, where the problem occurs.

namespace Mbx\Controller;

use Zend\Mvc\Controller\AbstractActionController;

class FooController extends AbstractActionController
{
    public function demoAction()
    {
        // DO STUFF
        return $this->redirect()->toRoute('imagine/there/is/a/route');
    }
}
namespace Mbx\Controller;

use Module\View\Model\FooDemoModel;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Zend\Http\Response;

class BarController extends AbstractActionController
{
    public function anotherDemoAction()
    {
        $result = $this->forward()->dispatch('Module\Controller\Foo', array('action' => 'demo'));

        /* DO MORE STUFF is not finished here, 
         * because the headers are sent on dispatch() (see above)
         * and so the redirect  
         * ...
         */

        if($result instanceof Response) {
            return $result;
        }

        $view = new ViewModel();
        if($result instanceof FooDemoModel) {
            $view->addChild($result, 'fooDemo');
        }

        return $view;
    }
}
Matthew Weier O'Phinney

I cannot recreate this issue, and, frankly, I'm not surprised.

Both the redirector's and the forward helper's methods return a response object. That is all they do. They do not render anything nor do they send the response. The only events that trigger that are triggered by the Application object, which is not even invoked at this time.

My guess is there is some other code happening somewhere in here that you're not including in the report.

Andriy Komm

I think I understood very well how this works - correct me if not.

I dont expect anything to be rendered. It would not make sense. Thats not what I want.

Also AbstractController specifies @return Response|mixed.

Thats correnct since you can return a ViewModel (or an extension of it) or array of variables which will be passed to the ViewModel constructor.

I only want the controller to be loaded with all its dependencies (thats why it takes the service name) and dispatched to get what it returns. Do some stuff, if the result is a response, return it to be used as response when the request is completed, or if its a specific view model, apply it as a child to the current view model. No thing more. No rendering.

I removed all the code and just placed a while(true); If the dispatched controller (in example the FooController) returns a Response, and i place a infinite loop after that, i still get redirected, even if this response is not further returned from the BarController... It happen after a few ms of execution time. So if your test was short, you will not notice it

Matthew Weier O'Phinney

I still can't recreate the issue, @ysor.

Here are the steps I took to recreate:

  • First, installed the skeleton application.
  • Second, created the controller Application\Controller\FooController as follows:
namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;

class FooController extends AbstractActionController
{
    public function demoAction()
    {
        // DO STUFF
        return $this->redirect()->toRoute('baz');
    }
}
  • Next, created Application\Controller\BarController:
namespace Application\Controller;

use Module\View\Model\FooDemoModel;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Zend\Http\Response;

class BarController extends AbstractActionController
{
    protected $log;

    public function setLog($log)
    {
        $this->log = $log;
    }

    public function anotherDemoAction()
    {
        $result = $this->forward()->dispatch('Application\Controller\Foo', array('action' => 'demo'));

        $this->log->info('This was logged within ' . __METHOD__);

        if ($result instanceof Response) {
            return $result;
        }

        $view = new ViewModel();
        return $view;
    }

    public function redirectAction()
    {
    }
}
  • Next, created a factory for Application\Controller\Bar in my Application\Module class:
    public function getControllerConfig()
    {
        return array('factories' => array(
            'Application\Controller\Bar' => function ($controllers) {
                $services = $controllers->getServiceLocator();
                $log      = $services->get('Application\Log');
                $controller = new Controller\BarController();
                $controller->setLog($log);
                return $controller;
            },
        ));
    }
  • Added an invokable definition for FooController in the module.config.php:
    'controllers' => array(
        'invokables' => array(
            'Application\Controller\Index' => 'Application\Controller\IndexController',
            'Application\Controller\Foo'   => 'Application\Controller\FooController',
        ),
    ),
  • Added routes "foo", "bar", and "baz" to the module.config.php routing configuration:
            'foo' => array(
                'type' => 'Segment',
                'options' => array(
                    'route'    => '/foo/:action',
                    'defaults' => array(
                        'controller' => 'Application\Controller\Foo',
                        'action'     => 'index',
                    ),
                ),
            ),
            'bar' => array(
                'type' => 'Literal',
                'options' => array(
                    'route'    => '/bar',
                    'defaults' => array(
                        'controller' => 'Application\Controller\Bar',
                        'action'     => 'anotherDemo',
                    ),
                ),
            ),
            'baz' => array(
                'type' => 'Literal',
                'options' => array(
                    'route'    => '/baz',
                    'defaults' => array(
                        'controller' => 'Application\Controller\Bar',
                        'action'     => 'redirect',
                    ),
                ),
            ),
  • Added logging configuration (this format is available in current master, upcoming 2.2.0 stable):
    'log' => array(
        'Application\Log' => array(
            'writers' => array(
                array(
                    'name' => 'Stream',
                    'options' => array(
                        'stream' => 'data/app.log',
                    ),
                ),
            ),
        ),
    ),
  • Finally, created two view scripts, view/application/bar/another-demo.phtml and view/application/bar/redirect.phtml:
<!-- another-demo.phtml -->
This is another demo
<!-- redirect.phtml -->
This is the redirect

If I go to the url "/foo/demo", I am redirected to "/baz". If I go to "/bar", I am also redirected to "/baz", AND I see a log message in data/app.log:

2013-05-13T14:58:11-05:00 INFO (6): This was logged within Application\Controller\BarController::anotherDemoAction

This demonstrates that the redirect is not happening before I return from the controller that called forward(). The redirection does happen, however, as that controller has set a Location header and returned a response -- which I then return, when detected, from BarController.

If you're not seeing this behavior, there is likely something in your own code that is causing it to happen; the above was done on a brand new install of the skeleton application.

Andriy Komm

I cant beleave this. I just did the same you did, grabbed the skeleton and added same configuration, controllers and view scripts (plus redirect.phtml, since the redirectAction do not return a Response).

And it works. What i cannot beleave:

After it worked I tested a case which I assumed when I first encountered this problem.
There are particular cases, when an Exception is thrown, but it does not get to the handler and you see nothing. For example sometimes it happen while creating services. Or sometimes when you use view helpers. Whenever strange things like that happen i wrap the code I suspect being responsible for that problem in a try-catch and output exceptions manually. So i did as first in my case and the test was negative. After i wasted about 5 hours figuring out what the hack is going on, even replacing it all with a while infine loop, which should kill everything but i got redirected still. I would like to see tomorrow @work how this was possible ...

So i added a simple throw new \Exception('test'); before the log in anotherDemoAction and i got redirected without any messages and without returning the result. Its like the handler is fetching the result from last dispatched controller.

As implemented here: https://github.com/zendframework/zf2/blob/master/library/Zend/Mvc/Application.php#L292-L301

So if your last dispatched Result is a Response with a location header, you wont see any Exceptions.

Matthew Weier O'Phinney

Again, that makes sense: the Response object has been given a Location header, so when it is rendered later, a redirect occurs. It may make sense to remove location headers if dispatch.error is triggered; however, we should likely discuss that on the zf-contributors mailing list before proceeding.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.