title: Single PHP "exit;" statement prevents HTML5 video in Safari
body: <p>Bizarre bug: in a PHP script that serves video files, I have a few test conditions (authenticate token, make sure file exists, etc.) before sending a "video/mp4" header and outputting an MP4 file.</p>
  
  <p>If any of the tests fail, <code>$fail</code> is given a non-false value.</p>
  
  <p>At the end of the tests there's this <code>if</code> statement:</p>
  
  <pre><code>if ($fail) {
      exit;
  }
  </code></pre>
  
  <p>This code works as expected in Chrome, but not in Safari.  However (and believe me, I've tested this every which way), if I simply comment out <code>exit;</code>, as in:</p>
  
  <pre><code>if ($fail) {
      //exit;
  }
  </code></pre>
  
  <p>... the code works perfectly in Safari — the video immediately starts loading.</p>
  
  <p>I'm sure that that <code>if</code> block is never entered, otherwise the script would stop executing, and I wouldn't be seeing the <code>video/mp4</code> header (not to mention, it wouldn't work in Chrome).  Further, whatever PHP is doing behind the scenes should be totally transparent to the browser.  I thought maybe there was some issue with output, but I would have received a warning if I had outputted anything before the headers.</p>
  
  <p>I've been seeing this behavior consistently for days — I've checked it probably 25 times in disbelief.</p>
  
  <p>Surely there's something I'm missing?</p>
  
  <p><strong>UPDATE</strong></p>
  
  <p>To clarify the problem, I changed the code a bit:</p>
  
  <pre><code>$fail = true;
  if ($fail) {
      die('Fail');
  }
  </code></pre>
  
  <p>Now we're guaranteed to hit the <code>die()</code> statement, and output "Fail".  Here are the headers, as seen by Safari:</p>
  
  <pre><code>Connection:Keep-Alive
  Content-Type:text/html
  Date:Thu, 24 Jun 2010 23:31:28 GMT
  Keep-Alive:timeout=10, max=29
  Server:Apache/2.2.15 (CentOS) mod_ssl/2.2.15 0.9.8l DAV/2 mod_auth_passthrough/2.1 FrontPage/5.0.2.2635
  Transfer-Encoding:Identity
  X-Powered-By:PHP/5.2.13
  </code></pre>
  
  <p>("Fail" is outputted as expected, too.)</p>
  
  <p>Now, when I comment-out <code>$fail = true;</code>, the headers change to:</p>
  
  <pre><code>Connection:Keep-Alive
  Content-Length:47406944
  Content-Type:video/mp4
  Date:Thu, 24 Jun 2010 23:32:58 GMT
  Keep-Alive:timeout=10, max=30
  Server:Apache/2.2.15 (CentOS) mod_ssl/2.2.15 0.9.8l DAV/2 mod_auth_passthrough/2.1 FrontPage/5.0.2.2635
  X-Powered-By:PHP/5.2.13
  </code></pre>
  
  <p>But the video still won't play!  (QuickTime logo with a question mark over it.)</p>
  
  <p>I think that's evidence enough that <code>$fail</code> remains false, and that <code>die()</code> is never executed.</p>
  
  <p>Now, get this: if I again comment out <code>die()</code> (functionally equivalent to <code>exit</code>), so that my final code is:</p>
  
  <pre><code>//$fail = true;
  if ($fail) {
      //die('Fail');
  }
  </code></pre>
  
  <p>... the video plays in Safari!</p>
  
  <p><strong>UPDATE 2</strong></p>
  
  <p>If I change the code to:</p>
  
  <pre><code>$fail = false;
  if ($fail) {
      die('Fail');
  }
  </code></pre>
  
  <p>... to absolutely guarantee that <code>$fail</code> is <code>false</code>, it plays in Safari!</p>
  
  <p>This behavior makes no sense to me, b/c if <code>$fail</code> was being set due to one of my verification conditions, then it would never output the <code>video/mp4</code> header, as when I explicitly set <code>$fail</code> to <code>true</code> — and instead it would output a <code>text/html</code> page with the word "Fail" — right?</p>
  
  <p><strong>UPDATE 3</strong></p>
  
  <p>Here's all of the relevant code, just to be totally clear:</p>
  
  <pre><code>// verify
  $fail = false;
  $token = false;
  $file_name = [rest assured that $file_name is correct];
  if (!$file_name) {
      $fail = true;
  } else {
      $file_name = '../video/'.$file_name;
  }
  if (!isset($_REQUEST['ts'])) {
      $fail = true;
  }
  if (isset($_POST['token']) || isset($_GET['token'])) {
      $token = isset($_POST['token']) ? $_POST['token'] : $_GET['token'];
  } else if (isset($_COOKIE['token'])) {
      $token = $_COOKIE['token'];
  }
  if ($token != md5(SALT_HASH.$_REQUEST['ts'])) {
      $fail = true;
  }
  if (((int)($_REQUEST['ts']) + 60 * 10) &lt; mktime()) {
      $fail = true;
  }
  if (!is_file($file_name)) {
      $fail = true;
  }
  if ($fail) {
      die('Fail');
  }
  
  // output
  $file_size = (string)(filesize($file_name));
  header('Content-Type: video/mp4');
  header('Content-Length: '.$file_size);
  readfile_chunked($file_name);
  exit;
  </code></pre>
  
  <p><strong>HOPEFULLY THE LAST UPDATE / SUMMARY</strong></p>
  
  <p>This question is probably already too long, but I thought I'd try one last time to summarize how weird this is.  There are 3 distinct responses:</p>
  
  <p>1) If I hardwire a <code>$fail = true;</code> before <code>if ($fail) die('Fail');</code>, just so I have a baseline for failure, I get a <code>text/html</code> header, and the word "Fail" is outputted as expected.</p>
  
  <p>2) If I leave the code as it is above, I get a <code>video/mp4</code> header, but a broken video in Safari (it will play in Chrome).</p>
  
  <p>3) Finally (and this is based on new testing I did today), if I comment out <code>$fail = true;</code> in the token-verification conditional, I get a <code>video/mp4</code> header, and the video plays in Safari.  Now, I figured there <em>must</em> something wrong with the token verification — but when I hardwire in another test to echo the value of $fail after the test, it's still false!  The conditional is never entered (I also just put a straight <code>die('!');</code> instead of <code>$fail = true;</code> — and I still get a <code>video/mp4</code> header).</p>
  
  <p>I simply cannot believe that commenting out code that never gets executed could cause a distinct response — and furthermore, that it <em>would</em> play in Chrome, but not in Safari; whatever's happening server-side with PHP should be completely transparent to the browser.</p>
  
  <p>Crazy.</p>
  
  <p><strong>AHA!</strong></p>
  
  <p>I added some logging to my script, and it turns out that when serving HTML5 video, the browser makes <em>two</em> requests.</p>
  
  <p>Here's the two (successful) requests from Chrome:</p>
  
  <pre><code>Fri Jun 25 17:41:22 2010 Browser: [Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.86 Safari/533.4] Fail: [0] Token: [83e50b519c0ed4662b6b7fabb8f6671e] Timestamp: [1277509282]
  Fri Jun 25 17:41:22 2010 Verification passed
  
  Fri Jun 25 17:41:22 2010 Browser: [Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.86 Safari/533.4] Fail: [0] Token: [83e50b519c0ed4662b6b7fabb8f6671e] Timestamp: [1277509282]
  Fri Jun 25 17:41:22 2010 Verification passed
  </code></pre>
  
  <p>And here's the two from Safari (first successful, 2nd fails):</p>
  
  <pre><code>Fri Jun 25 17:41:32 2010 Browser: [Apple Mac OS X v10.6.4 CoreMedia v1.0.0.10F569] Fail: [0] Token: [6374fba3d9eac7d94de9741db76953c6] Timestamp: [1277509291]
  Fri Jun 25 17:41:32 2010 Verification passed
  
  Fri Jun 25 17:41:33 2010 Browser: [QuickTime/7.6.6 (qtver=7.6.6;cpu=IA32;os=Mac 10.6.4)] Fail: [1] Token: [] Timestamp: [1277509291]
  </code></pre>
  
  <p>I have to go out right now, but I'm almost certain this behavior is the root of the bug.  For some reason, the 2nd Safari request can't find the token.</p>