Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ResponseListener in Symfony leads to duplicate header 'Content-Type' from apache in dev mode

I'm running a Symfony app on a MAMP PRO setup on a Mac. Within my symfony app I use a ResponseListener which contains the following function:

/**
 * Handle OPTIONS calls and add Access-Control headers.
 *
 * @param FilterResponseEvent $event Filter response event
 */
public function onKernelResponse(FilterResponseEvent $event)
{
    // Don't do anything if it's not the master request.
    if (!$event->isMasterRequest()) {
        return;
    }

    $request = $event->getRequest();
    if ($request->getRealMethod() == Request::METHOD_OPTIONS) {
        $response = new Response();
        $response->headers->set('Access-Control-Allow-Origin', '*');
        $response->headers->set('Access-Control-Allow-Headers', 'Origin, X-Requested-With, X-Auth-Token, X-App-Version, Content-Type, Accept, Authorization');
        $response->headers->set('Access-Control-Allow-Methods', 'POST, GET, PUT');
        $event->setResponse($response);
    } else {
        $response = $event->getResponse();

        $response->headers->set('Access-Control-Allow-Origin', '*');
        $response->headers->set('Access-Control-Allow-Credentials', true);
        $response->headers->set('Access-Control-Allow-Headers', 'Origin, X-Requested-With, X-Auth-Token, X-App-Version, Content-Type, Accept, Authorization');
        $response->headers->set('Access-Control-Allow-Methods', 'POST, GET, PUT');
    }
}

It's just a solution to develop a Ionic app in the browser and handle the OPTIONS calls from the app without getting errors there. But it's just an example, I have the same issue with other Symfony apps that use a ResponseListener.

The above example works perfectly if I run it in the prod environment or in the dev environment if no error occurs. But as soon as I get an PHP error it results in the following:

Internal Server Error

The server encountered an internal error or misconfiguration and was unable to complete your request.

Please contact the server administrator, [email protected] and inform them of the time the error occurred, and anything you might have done that may have caused the error.

More information about this error may be available in the server error log.

Additionally, a 500 Internal Server Error error was encountered while trying to use an ErrorDocument to handle the request.

If I look into the apache_error.log it says:

FastCGI: comm with server "/Applications/MAMP/fcgi-bin/php7.0.13.fcgi" aborted: error parsing headers: duplicate header 'Content-Type', referer: http://app.domain/app_dev.php/my-route

This only seems to happen for PHP errors. If I for example throw a NotFoundHttpException with

throw new NotFoundHttpException("Test exception");

I get the usual Symfony error page.

If I do something like

new ClassDoesNotExist();

it results in the Internal Server error.

I know that it is due to the ResponseListener because the Symfony error page is shown as soon as I put an exit; on the top of the function like this:

public function onKernelResponse(FilterResponseEvent $event)
{
    exit;

    // Don't do anything if it's not the master request.
    if (!$event->isMasterRequest()) {
        return;
    }
    ...

Is there something wrong with the ResponseListener? Is there some kind of configuration that can be done on apache that this error is not thrown?

EDIT:

Here is the MAMP PRO vhost config:

<VirtualHost *:80>
ServerName project.localhost

DocumentRoot "/Users/MyUser/Projekte/MyProject/web"

<IfModule xsendfile_module>
    XSendFilePath "/Users/MyUser/Projekte/MyProject/web"
</IfModule>

<Directory "/Users/MyUser/Projekte/MyProject/web">
    Options Includes FollowSymLinks ExecCGI
    AllowOverride All
    <IfModule authz_host_module>
        Order allow,deny
        Allow from all
    </IfModule>

</Directory>

WSGIDaemonProcess project.localhost processes=2 threads=15
WSGIProcessGroup project.localhost
WSGIScriptAlias /project.localhostWsgiApp "/Users/MyUser/Projekte/MyProject/web/wsgiapp.py"
AddHandler php-fastcgi .php
Action php-fastcgi "/fcgi-bin/php7.0.13.fcgi"

EDIT 2:

As suggested by @mickadoo it could be possible that another Listener is adding the duplicate header Content-Type. If I stop the propagation I get the Symfony error message I want. But my listener is also the first being called, so this would prevent any other listener from ever being triggered. Executing debug:event-dispatcher results in the following:

"kernel.response" event
-----------------------

 ------- -------------------------------------------------------------------------------------------- ----------
Order   Callable                                                                                     Priority
 ------- -------------------------------------------------------------------------------------------- ----------
  #1      MyCompany\Bundle\AppBundle\EventListener\ResponseListener::onKernelResponse()                   0
  #2      Sonata\BlockBundle\Cache\HttpCacheHandler::onKernelResponse()                                0
  #3      Symfony\Component\HttpKernel\EventListener\ResponseListener::onKernelResponse()              0
  #4      Symfony\Bundle\FrameworkBundle\DataCollector\RequestDataCollector::onKernelResponse()        0
  #5      Symfony\Component\Security\Http\RememberMe\ResponseListener::onKernelResponse()              0
  #6      Sensio\Bundle\FrameworkExtraBundle\EventListener\HttpCacheListener::onKernelResponse()       0
  #7      Symfony\Component\HttpKernel\EventListener\ProfilerListener::onKernelResponse()              -100
  #8      Symfony\Bundle\WebProfilerBundle\EventListener\WebDebugToolbarListener::onKernelResponse()   -128
  #9      Symfony\Component\HttpKernel\EventListener\SaveSessionListener::onKernelResponse()           -1000
  #10     Symfony\Component\HttpKernel\EventListener\StreamedResponseListener::onKernelResponse()      -1024
 ------- -------------------------------------------------------------------------------------------- --------

When I add another response listener with priority -2048 to make him be the last one triggered and use the following method there:

/**
 * @param FilterResponseEvent $event Filter response event
 */
public function onKernelResponse(FilterResponseEvent $event)
{
    // Don't do anything if it's not the master request.
    if (!$event->isMasterRequest()) {
        return;
    }

    $response = $event->getResponse();

    echo "<pre>";
    print_r($response->headers->all());
    exit;
}

It returns:

Array
(
    [cache-control] => Array
    (
        [0] => no-cache, private
    )
    [access-control-allow-origin] => Array
    (
        [0] => *
    )
    [access-control-allow-credentials] => Array
    (
        [0] => 1
    )
    [access-control-allow-headers] => Array
    (
        [0] => Origin, X-Requested-With, X-Auth-Token, X-App-Version, Content-Type, Accept, Authorization
    )
    [access-control-allow-methods] => Array
    (
        [0] => POST, GET, PUT
    )
    [content-type] => Array
    (
        [0] => text/html; charset=UTF-8
    )
    [x-debug-token] => Array
    (
        [0] => 2f5dbb
    )
    [x-debug-token-link] => Array
    (
        [0] => http://app.project.localhost/app_dev.php/_profiler/2f5dbb
    )
)

But there still is no additional Content-Type. Any ideas?

EDIT 3:

Another strange thing is the following:

If I use the $event->stopPropagation() at the end of my method and add the listener with priority 0 everything works (meaning the Symfony error page is shown), if I set it with priority -2048 I get the Apache error message. But even if I output all headers at the end of it there is no duplicate header. I don't really get how this is possible. How can there be multiple of the same header being returned to apache with just one request and all headers at the end are unique in the output?

like image 717
Christian Kolb Avatar asked Feb 03 '17 20:02

Christian Kolb


1 Answers

How about set the response to the event?

I mean on the response given by $event->getResponse() you add the headers, but you don't $event->setResponse($response) on the else part.

like image 177
jeremy Avatar answered Oct 14 '22 14:10

jeremy