title: Missing CORS headers in response from PHP/Apache2
body: <p>A Zend Expressive project my company is working on is ready to be shipped but in our staging environment we seem to be missing response headers for a CORS pre-flight request. This does not happen in our development environment. We're using <a href="https://github.com/tuupola/cors-middleware" rel="nofollow noreferrer">CorsMiddleware</a> in our pipeline but it doesn't look like that middleware is the culprit.</p>
  
  <h1>The problem</h1>
  
  <p>During runtime, the middleware detects incoming pre-flight requests and it will reply with a response like so:</p>
  
  <pre><code>HTTP/1.1 200 OK
  Date: Mon, 20 Aug 2018 15:09:03 GMT
  Server: Apache
  X-Powered-By: PHP/7.1.19
  Access-Control-Allow-Origin: https://example.com
  Vary: Origin
  Access-Control-Allow-Headers: content-type
  Keep-Alive: timeout=5, max=100
  Connection: Keep-Alive
  Transfer-Encoding: chunked
  Content-Type: text/html; charset=UTF-8
  </code></pre>
  
  <p>Well, that only works on our development servers and php's built-in webservers. The response is different from our staging server, even though the request is exactly the same, apart from the host:</p>
  
  <pre><code>HTTP/1.1 200 OK
  Date: Mon, 20 Aug 2018 15:11:29 GMT
  Server: Apache
  Keep-Alive: timeout=5, max=100
  Cache-Control: max-age=0, no-cache
  Content-Length: 0
  Content-Type: text/html; charset=UTF-8
  </code></pre>
  
  <h1>What we've tried</h1>
  
  <h3>Investigating the middleware</h3>
  
  <p>We've verified that CorsMiddleware runs perfectly fine and actually sets the required headers. When we modify CorsMiddleware's response code and set it to <code>202</code> instead of <code>200</code> we now <em>do</em> get the headers we're looking for. Changing the response code back to <code>200</code> makes the headers disappear again.</p>
  
  <h3>Setting the headers manually</h3>
  
  <p>Using the following example:</p>
  
  <pre><code>header('Access-Control-Allow-Origin: https://example.com');
  header('Access-Control-Allow-Headers: content-type');
  header('Vary: Origin');
  exit(0);
  </code></pre>
  
  <p>This has the same behavior until we modify the response code to <code>204</code> or anything other than <code>200</code>.</p>
  
  <h3>Looking at the body</h3>
  
  <p>The response body is empty and shouldn't contain anything but when we add content to the response body the headers appear as if nothing was wrong.</p>
  
  <p>So if I add body content, the headers are present. No body content? No CORS headers. Is this some setting in Apache? Am I missing some configuration in PHP? Am I forgetting anything?</p>
  
  <h1>Further details</h1>
  
  <p>All requests have been tested with httpie, Postman, curl and PhpStorm's http client.</p>
  
  <p>Here's the httpie example:</p>
  
  <pre><code>http -v OPTIONS https://staging.****.com \
      'access-control-request-method:POST' \
      'origin:https://example.com' \
      'access-control-request-headers:content-type'
  </code></pre>
  
  <p>Here's the curl example:</p>
  
  <pre><code>curl "https://staging.****.com" \
  --request OPTIONS \
  --include \
  --header "access-control-request-method: POST" \
  --header "origin: https://example.com" \
  --header "access-control-request-headers: content-type"
  </code></pre>
  
  <h3>Cors configuration in pipeline.php (wildcard only for testing):</h3>
  
  <pre><code>$app-&gt;pipe(new CorsMiddleware([
      "origin"         =&gt; [
          "*",
      ],
      "headers.allow"  =&gt; ['Content-Type'],
      "headers.expose" =&gt; [],
      "credentials"    =&gt; false,
      "cache"          =&gt; 0,
  
      // Get list of allowed methods from matched route or provide empty array.
      'methods' =&gt; function (ServerRequestInterface $request) {
          $result = $request-&gt;getAttribute(RouteResult::class);
          /** @var \Zend\Expressive\Router\Route $route */
          $route = $result-&gt;getMatchedRoute();
  
          return $route ? $route-&gt;getAllowedMethods() : [];
      },
  
      // Respond with a json response containing the error message when the CORS check fails.
      'error'   =&gt; function (
          ServerRequest $request,
          Response $response,
          $arguments
      ) {
          $data['status']  = 'error';
          $data['message'] = $arguments['message'];
  
          return $response-&gt;withHeader('Content-Type', 'application/json')
                          -&gt;getBody()-&gt;write(json_encode($data));
      },
  ]);
  </code></pre>
  
  <h3>The staging environment:</h3>
  
  <pre><code>OS: Debian 9.5 server
  Webserver: Apache/2.4.25 (Debian) (built: 2018-06-02T08:01:13)
  PHP: PHP 7.1.20-1+0~20180725103315.2+stretch~1.gbpd5b650 (cli) (built: Jul 25 2018 10:33:20) ( NTS )
  </code></pre>
  
  <h3>Apache2 vhost on staging:</h3>
  
  <pre><code>&lt;IfModule mod_ssl.c&gt;
  &lt;VirtualHost ****:443&gt;
          ServerName staging.****.com
          DocumentRoot /var/www/com.****.staging/public
  
          ErrorLog /var/log/apache2/com.****.staging.error.log
          CustomLog /var/log/apache2/com.****.staging.access.log combined
          &lt;Directory /var/www/com.****.staging&gt;
                  Options +SymLinksIfOwnerMatch
                  AllowOverride All
                  Order allow,deny
                  allow from all
          &lt;/Directory&gt;
  SSLCertificateFile /etc/letsencrypt/live/staging.****.com/fullchain.pem
  SSLCertificateKeyFile /etc/letsencrypt/live/staging.****.com/privkey.pem
  Include /etc/letsencrypt/options-ssl-apache.conf
  &lt;/VirtualHost&gt;
  &lt;/IfModule&gt;
  </code></pre>
  
  <h3>Apache2 vhost on development:</h3>
  
  <pre><code>&lt;VirtualHost *:443&gt;
          ServerName      php71.****.com
          ServerAdmin     dev@****.com
          DocumentRoot    /var/www/
  
          &lt;Directory /var/www/&gt;
                  Options Indexes FollowSymlinks
                  AllowOverride All
                  Require all granted
          &lt;/Directory&gt;
  
          ErrorLog        ${APACHE_LOG_DIR}/error.ssl.log
          CustomLog       ${APACHE_LOG_DIR}/access.ssl.log combined
  
          SSLEngine On
          SSLCertificateFile /etc/ssl/certs/****.crt
          SSLCertificateKeyFile /etc/ssl/certs/****.key
  &lt;/VirtualHost&gt;
  </code></pre>
  
  <h1>To everybody pointing fingers to Cloudflare:</h1>
  
  <p>Try this direct link with httpie. This link is not using cloudflare:</p>
  
  <pre><code>http -v OPTIONS http://37.97.135.33/cors.php \
      'access-control-request-method:POST' \
      'origin:https://example.com' \
      'access-control-request-headers:content-type'
  </code></pre>
  
  <p>Check the source code in your browser: <a href="http://37.97.135.33/cors.php?source=1" rel="nofollow noreferrer">http://37.97.135.33/cors.php?source=1</a></p>