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

2

7

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?

Stonedeaf answered 12/2, 2016 at 11:43 Comment(3)
With the amount of exceptions in the manual and the lack of portability, I'd recommend simply saving the result after reading it the first time. After this you can seek however much you want with the result (and you don't have to save it to a file - php://memory should do it.)Many
@Many thank you, I gave up of "fixing" this issue and ended with a plan to avoid reading input more than once in all mine future projects, like you suggested. If you convert your comment to an answer I will accept it.Stonedeaf
You're welcome - give me a sec and I'll have an answer up.Many
M
3

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();
Many answered 15/2, 2016 at 8:46 Comment(0)
T
2

Another approach might be to open the input stream each time instead of rewinding and seeking.

$input = fopen('php://input', 'r');
echo 'First attempt: ' . fread($input, 1024) . '<br>';
$input2 = fopen('php://input', 'r');
echo 'Second attempt: ' . fread($input2, 1024) . '<br>';

If the resource cost won't a problem.

Also there's file_get_contents

$input = file_get_contents("php://input");
$input = json_decode($input, TRUE);

if you're sending json.

Thunderstorm answered 12/2, 2016 at 12:3 Comment(1)
Yes, I already noticed it works in that case but it is not suitable for me because I'm using "zend-diactoros" component instead of direct stream access (my example was simplified to avoid confusion). This component abstracts PHP input stream with (string)$request->getBody() so I really don't want to bypass zend-diactoros and open stream manually.Stonedeaf

© 2022 - 2024 — McMap. All rights reserved.