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.
If any of the tests fail, $fail
is given a non-false value.
At the end of the tests there's this if
statement:
if ($fail) {
exit;
}
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 exit;
, as in:
if ($fail) {
//exit;
}
... the code works perfectly in Safari — the video immediately starts loading.
I'm sure that that if
block is never entered, otherwise the script would stop executing, and I wouldn't be seeing the video/mp4
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.
I've been seeing this behavior consistently for days — I've checked it probably 25 times in disbelief.
Surely there's something I'm missing?
UPDATE
To clarify the problem, I changed the code a bit:
$fail = true;
if ($fail) {
die('Fail');
}
Now we're guaranteed to hit the die()
statement, and output "Fail". Here are the headers, as seen by Safari:
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
("Fail" is outputted as expected, too.)
Now, when I comment-out $fail = true;
, the headers change to:
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
But the video still won't play! (QuickTime logo with a question mark over it.)
I think that's evidence enough that $fail
remains false, and that die()
is never executed.
Now, get this: if I again comment out die()
(functionally equivalent to exit
), so that my final code is:
//$fail = true;
if ($fail) {
//die('Fail');
}
... the video plays in Safari!
UPDATE 2
If I change the code to:
$fail = false;
if ($fail) {
die('Fail');
}
... to absolutely guarantee that $fail
is false
, it plays in Safari!
This behavior makes no sense to me, b/c if $fail
was being set due to one of my verification conditions, then it would never output the video/mp4
header, as when I explicitly set $fail
to true
— and instead it would output a text/html
page with the word "Fail" — right?
UPDATE 3
Here's all of the relevant code, just to be totally clear:
// 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) < 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;
HOPEFULLY THE LAST UPDATE / SUMMARY
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:
1) If I hardwire a $fail = true;
before if ($fail) die('Fail');
, just so I have a baseline for failure, I get a text/html
header, and the word "Fail" is outputted as expected.
2) If I leave the code as it is above, I get a video/mp4
header, but a broken video in Safari (it will play in Chrome).
3) Finally (and this is based on new testing I did today), if I comment out $fail = true;
in the token-verification conditional, I get a video/mp4
header, and the video plays in Safari. Now, I figured there must 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 die('!');
instead of $fail = true;
— and I still get a video/mp4
header).
I simply cannot believe that commenting out code that never gets executed could cause a distinct response — and furthermore, that it would play in Chrome, but not in Safari; whatever's happening server-side with PHP should be completely transparent to the browser.
Crazy.
AHA!
I added some logging to my script, and it turns out that when serving HTML5 video, the browser makes two requests.
Here's the two (successful) requests from Chrome:
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
And here's the two from Safari (first successful, 2nd fails):
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]
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.