Skip to content

readfile() problems if user closes browser tab #16422

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
jjdunn opened this issue Oct 13, 2024 · 3 comments
Closed

readfile() problems if user closes browser tab #16422

jjdunn opened this issue Oct 13, 2024 · 3 comments

Comments

@jjdunn
Copy link

jjdunn commented Oct 13, 2024

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

@cmb69
Copy link
Member

cmb69 commented Oct 13, 2024

Yeah, this may be a Windows specific issue.

Anyhow, did you try to set ignore_user_abort=1 or call ignore_user_abort(); that should (if it works) alleviate the problem. And for the general case, there may be nothing more we can do.

For tmpfile() is might be a good idea to set FILE_FLAG_DELETE_ON_CLOSE (or similar on non Windows systems).

@jjdunn
Copy link
Author

jjdunn commented Oct 13, 2024

Yeah, this may be a Windows specific issue.

Anyhow, did you try to set ignore_user_abort=1 or call ignore_user_abort(); that should (if it works) alleviate the problem. And for the general case, there may be nothing more we can do.

ini_set('ignore_user_abort', true); - solves the presenting problems. temp file is deleted even if browser tab is closed.

ignore_user_abort(true); called immediately before readfile(); also fixes the problem.

@cmb69 - thank you for these suggestions!! I was not aware of this option.

For tmpfile() is might be a good idea to set FILE_FLAG_DELETE_ON_CLOSE (or similar on non Windows systems).

https://www.php.net/manual/en/function.tmpfile.php says "Caution: If the script terminates unexpectedly, the temporary file may not be deleted. ". that is the observed behavior. Are you proposing the change that ?

Personally I don't see anything wrong in tmpfile() - I think the incorrect behavior is in readfile() if the user agent is aborted; but it can be addressed with ignore_user_abort() as suggested.

I'm fine with that solution; leaving this ticket open for others to judge if correction is needed in readfile().

@cmb69
Copy link
Member

cmb69 commented Feb 4, 2025

Hmm, no further feedback for month, so I'm closing this ticket.

@cmb69 cmb69 closed this as not planned Won't fix, can't repro, duplicate, stale Feb 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants