Search code examples
phpphp-stream-wrappers

php://input can only be read once in PHP 5.6.16


PHP manual states that a stream opened with php://input support seek operation and can be read multiple times as of PHP 5.6, but I can't make it work. The following example clearly shows it doesn't work:

<!DOCTYPE html>
<html>
<body>
<form method="post">
<input type="hidden" name="test_name" value="test_value">
<input type="submit">
</form>
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST')
{
    $input = fopen('php://input', 'r');
    echo 'First attempt: ' . fread($input, 1024) . '<br>';
    if (fseek($input, 0) != 0)
        exit('Seek failed');
    echo 'Second attempt: ' . fread($input, 1024) . '<br>';
}
?>
</body>
</html>

Output:

First attempt: test_name=test_value
Second attempt: 

php://input stream was

  1. successfully read
  2. successfully rewinded (fseek succeeded)
  3. unsuccessfully read

Am I doing something wrong?


Solution

  • With the amount of exceptions and lack of portability using php://input I'd recommend you to read the stream and save it to another stream to avoid unexpected behaviour.

    You can use php://memory in order to create a file-stream-like wrapper, which will give you all the same functionality that php://input should have without all of the annoying behaviour.

    Example:

    <?php
    
    $inputHandle = fopen('php://memory', 'r+');
    
    fwrite($inputHandle, file_get_contents('php://input'));
    
    fseek($inputHandle, 0);
    

    Additionally you can create your own class to refer to this object consistently:

    <?php
    
    class InputReader {
        private static $instance;
    
        /**
         * Factory for InputReader
         *
         * @param string $inputContents
         *
         * @return InputReader
         */
        public static function instance($inputContents = null) {
            if (self::$instance === null) {
                self::$instance = new InputReader($inputContents);
            }
    
            return self::$instance;
        }
    
        protected $handle;
    
        /**
         * InputReader constructor.
         *
         * @param string $inputContents
         */
        public function __construct($inputContents = null) {
            // Open up a new memory handle
            $this->handle = fopen('php://memory', 'r+');
    
            // If we haven't specified the input contents (in case you're reading it from somewhere else like a framework), then we'll read it again
            if ($inputContents === null) {
                $inputContents = file_get_contents('php://input');
            }
    
            // Write all the contents of php://input to our memory handle
            fwrite($this->handle, $inputContents);
    
            // Seek back to the start if we're reading anything
            fseek($this->handle, 0);
        }
    
        public function getHandle() {
            return $this->handle;
        }
    
        /**
         * Wrapper for fseek
         *
         * @param int $offset
         * @param int $whence
         *
         * @return InputReader
         *
         * @throws \Exception
         */
        public function seek($offset, $whence = SEEK_SET) {
            if (fseek($this->handle, $offset, $whence) !== 0) {
                throw new \Exception('Could not use fseek on memory handle');
            }
    
            return $this;
        }
    
        public function read($length) {
            $read = fread($this->handle, $length);
    
            if ($read === false) {
                throw new \Exception('Could not use fread on memory handle');
            }
    
            return $read;
        }
    
        public function readAll($buffer = 8192) {
            $reader = '';
    
            $this->seek(0); // make sure we start by seeking to offset 0
    
            while (!$this->eof()) {
                $reader .= $this->read($buffer);
            }
    
            return $reader;
        }
    
        public function eof() {
            return feof($this->handle);
        }
    }
    

    Usage:

    $first1024Bytes = InputReader::instance()->seek(0)->read(1024);
    $next1024Bytes = InputReader::instance()->read(1024);
    

    Usage (read all):

    $phpInput = InputReader::instance()->readAll();