Skip to content

readfile() problems if user closes browser tab #16422

Closed as not planned
Closed as not planned
@jjdunn

Description

@jjdunn

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:

  1. if the target tab is open, the PDF will be returned; and the temp file will be deleted
  2. 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:

  1. the fatal condition encountered by readfile() when the browser tab is closed:
    a. cannot be caught
    b. is not reported by error_get_last()
    c. does not show in any log file
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions