Closed as not planned
Description
Description
Normal situation:
- user clicks a link (or button) in a web page, which is intended to return a PDF in a new browser tab.
- the PDF-generation code creates the PDF in the sys_temp_dir
- the PDF-generation code uses readfile() to return the generated file
- the PDF is returned to the browser as an attachment
- the PDF-generation code deletes the temporary/generated PDF in the sys_temp_dir
Abnormal situation causing the problem:
- the user closes the target browser tab before the file is returned
Expectations:
- if the target tab is open, the PDF will be returned; and the temp file will be deleted
- if the target tab is closed before the PDF can be returned, the code will continue to delete the temp file
What actually happens if the user closes the target tab before the PDF is returned:
- readfile() encounters some kind of fatal error which cannot be caught
- the defined shutdown function is invoked; but
error_get_last()
returns nothing - the
__destruct()
method on the PDF-generation class runs, but is unable to delete the temp PDF because the file is LOCKED - a PHP "Permission Denied" Warning is generated in the log, about the failure to unlink the temp PDF file
Analysis:
- If the sample code below is run in a debugger/IDE, and a breakpoint is set just before
unlink()
; then use Windows Resource Monitor to find out which process is locking the temp PDF file: apache httpd is holding a lock on the temp file. - it appears that
readfile()
fails since the browser connection no longer exists; but it has obtained a lock on the temp PDF file which is not released until execution finishes
PROBLEMS:
- the fatal condition encountered by
readfile()
when the browser tab is closed:
a. cannot be caught
b. is not reported byerror_get_last()
c. does not show in any log file readfile()
obtains a lock on the file to be returned, but does not release the lock when the fatal condition occurs
CAVEAT:
- so far this problem has only been observed on a Windows system running apache httpd. I don't know if it can be replicated on a different OS, or using a different web server
The following code can be used to replicate the problem:
<?php
/*
* setup: save this code to your web server root directory as "readfileTest.php":
*
* normal usage:
* 1. copy any valid PDF to sysTempDir/test.pdf
* 2. open a browser to http://YOURHOST/readfileTest.php
* 3. click the link, new tab opens
* 4. wait 10 seconds; PDF is delivered
* 5. PDF is deleted from sysTempDir
*
* abnormal case:
* 1. copy any valid PDF to sysTempDir/test.pdf
* 2. open a browser to http://YOURHOST/readfileTest.php
* 3. click the link, new tab opens
* 4. immediately close the newly-opened tab
* 5. wait 10 seconds
* 6. notice the PDF is *not* deleted from sysTempDir
* 7. web server log has PHP Warning "Permission Denied" on sysTempDir/test.pdf
* 8. NO other output in the web server log, from any of the error_log() calls
*/
ini_set('error_reporting', E_ALL);
register_shutdown_function('myShutdown');
$testFileName = 'test.pdf'; // must exist in sysTempDir
$pause = 10; // seconds
if (isset($_GET['showFile'])) {
$file = $_GET['showFile'];
$RF = new RFTest($file);
sleep($pause); // give user time to close browser tab
$RF->showFile();
} else {
// render the link
$showFileUrl = $_SERVER['HTTP_HOST'] . '/readfileTest.php?showFile=' . $testFileName;
echo '<a target="_blank" href="https://'.$showFileUrl.'">open the file</a>';
}
class RFTest {
private $fileName;
function getFilePath() {
return sys_get_temp_dir() . DIRECTORY_SEPARATOR . $this->fileName;
}
function __construct($fileName) {
$this->fileName = $fileName;
$fullPath = $this->getFilePath();
if (! file_exists($fullPath)) {
error_log("file $fullPath does not exist");
exit (1);
}
}
function showFile() {
try {
$fullPath = $this->getFilePath();
$this->sendHeaders($fullPath, $this->fileName);
readfile($fullPath);
} catch (Throwable $t) {
// NEVER ENTERED
error_log("caught Throwable in showFile()" . print_r($t, true));
exit (1);
}
}
function sendHeaders($filePath, $fileName) {
header('Content-Type: application/pdf');
header('Content-Length:'.filesize($filePath));
header('Content-Transfer-Encoding: binary');
header("Content-Disposition: attachment; filename=$fileName; filename*=UTF-8''$fileName");
}
function __destruct() {
try {
$fullPath = $this->getFilePath();
if (file_exists($fullPath)) {
// SET A BREAKPOINT HERE; then use appropriate tools to find which process is locking the file
unlink($fullPath);
}
} catch (Throwable $t) {
// NEVER ENTERED
error_log( "caught Throwable in __destruct()" . print_r($t, true));
exit (1);
}
}
}
function myShutdown () {
$error = error_get_last();
if (!empty($error)) {
// NEVER ENTERED
error_log("caught error in shutdown function" . print_r($error, true));
exit (1);
}
}
PHP Version
PHP 8.3.11 x64
Operating System
Windows 10 Pro; apache httpd 2.4.62