为什么 php://input 可以多次阅读,尽管文档另有说明?

Why can php://input be read more than once despite the documentation saying otherwise?

提问人:deceze 提问时间:6/24/2010 最后编辑:deceze 更新时间:9/14/2013 访问量:4839

问:

PHP 文档指出只能读取一次。php://input

在我的应用程序中,我需要读取它两次,一次用于身份验证目的,一次用于实际处理内容,这两个功能都由不同的独立模块处理。疯狂的是:它有效

我可以指望它在任何地方都能工作吗,或者这是我的 PHP 版本 (5.2.10) 中的侥幸?我能找到的唯一关于这一点的文档是声明它不应该工作的文档,没有提到版本限制。


按照Dennis的预感,我做了这个测试:

$in = fopen('php://input', 'r');
echo fread($in, 1024) . "\n";
fseek($in, 0);
echo fread($in, 1024) . "\n";
fclose($in);
echo file_get_contents('php://input') . "\n";

冰壺:

$ curl http://localhost:8888/tests/test.php -d "This is a test"
This is a test

This is a test

显然,它仅限于每个打开的句柄读取一次。


再深入挖掘一下就会发现,对于 PUT 请求,确实只能读取一次。上面的示例使用了 POST 请求。php://input

php 输入流

评论

6赞 Alex Shesterov 1/13/2015
...而现在,4.5 年后,PHP 5.6 正式支持多次读取,甚至寻求操作:)php://input
2赞 Umbrella 6/9/2015
对于我们这些尚未使用 PHP 5.6 的人来说,包装一个缓存结果的函数并调用它是一种可行的解决方法。file_get_contents('php://input')

答:

3赞 Dennis Haarbrink 6/24/2010 #1

也许它们的意思是 fseek() 或 rewind() 不可用。 您是否在打开的 php://input 上尝试过这些功能之一?

评论

0赞 Dennis Haarbrink 6/30/2010
也许我忽略了一些东西,但是......新的问题是什么?
24赞 Artefacto 8/4/2010 #2

对源代码进行一些检查就会得出答案。

首先,是的,每个句柄只能读取一次,因为基础流未实现处理程序:seek

php_stream_ops php_stream_input_ops = {
    php_stream_input_write,
    /* ... */
    "Input",
    NULL, /* seek */
    /* ... */
};

其次,读取处理程序具有两种不同的行为,具体取决于“POST 数据”是否已被读取并存储在 中。SG(request_info).raw_post_data

if (SG(request_info).raw_post_data) {
    read_bytes = SG(request_info).raw_post_data_length - *position;
    /* ...*/
    if (read_bytes) {
        memcpy(buf, SG(request_info).raw_post_data + *position, read_bytes);
    }
} else if (sapi_module.read_post) {
    read_bytes = sapi_module.read_post(buf, count TSRMLS_CC);
    /* ... */
} else {
    stream->eof = 1;
}

因此,我们这里有三种可能性:

  1. 请求正文数据已被读取并存储在 中。在这种情况下,由于存储了数据,我们可以打开并读取 的多个句柄。SG(request_info).raw_post_dataphp://input
  2. 请求正文数据已被读取,但其内容未存储在任何地方。 不能给我们任何东西。php://input
  3. 尚未读取请求数据。这意味着我们只能打开和读取一次。php://input

注意:以下是默认行为。不同的 SAPI 或其他扩展可能会更改此行为。

在 POST 请求的情况下,PHP 会根据内容类型定义不同的 POST 阅读器和 POST 处理程序。

案例 1.当我们有 POST 请求时,就会发生这种情况:

  • 使用 content-type application/x-www-form-encoded。 检测具有 content-type 的 POST 请求并调用 。这将检测内容类型并定义 POST 读取器/处理程序对。POST 读取器是 ,它会立即被调用,只是将请求正文复制到 。然后调用默认的后处理器,如果设置了 ini 设置,则会填充,然后复制并清除第一个阅读器。对处理程序的调用在这里无关紧要,并且会延迟到构建超全局变量(如果激活了 JIT 并且未使用超全局变量,则可能不会发生这种情况)。sapi_activatesapi_read_post_datasapi_read_standard_form_dataSG(request_info).post_dataphp_default_post_reader$HTTP_RAW_POST_DATAalways_populate_post_dataSG(request_info).post_dataSG(request_info).raw_post_data
  • 使用无法识别或不存在的内容类型。在本例中,没有定义的 POST 读取器和处理程序。这两种情况最终都没有读取任何数据。由于这是一个 POST 请求,并且没有读取器/处理程序对,因此将被调用。这与内容类型的读取处理程序相同,因此所有数据都会被吞噬。从现在开始,唯一的区别是始终填充(无论 的值如何),并且没有用于构建超全局变量的处理程序。php_default_post_readersapi_read_standard_form_dataapplication/x-www-form-encodedSG(request_info).post_data$HTTP_RAW_POST_DATAalways_populate_post_data

案例 2.当我们有一个内容类型为“multipart/form-data”的表单请求时,就会发生这种情况。POST 读取器是 ,因此处理程序充当混合 .在该阶段不会读取任何数据。该函数最终在稍后阶段调用,该阶段又调用 POST 处理程序。 读取请求数据,填充 和 ,但在 中不保留任何内容。NULLrfc1867_post_handlerreader/handlersapi_activatesapi_handle_postrfc1867_post_handlerPOSTFILESSG(request_info).raw_post_data

案例 3.最后一种情况发生在与 POST 不同的请求(例如 PUT)中。 被直接调用。由于该请求不是 POST 请求,因此数据被 吞噬。由于没有读取任何数据,因此无需执行任何操作。php_default_post_readersapi_read_standard_form_data

评论

0赞 deceze 8/4/2010
非常详细的答案!但是,这种行为是否有充分的理由,或者仅仅是一种疏忽?即它是“按预期”还是“按编码”工作?:)
1赞 Artefacto 8/4/2010
@deceze 实施似乎是合理的。application/x-www-form-encoded 数据通常很短,因此将其保留在内存中的内存损失很小。multipart/form-data 通常要大得多(例如包含文件),让它内存会更繁重。对于其他 POST 请求,我认为将所有内容都保留在内存中不是一个好主意,但这是向后兼容性所必需的(旧脚本只有 $HTTP_RAW_POST_DATA)。至于其他请求方法,实现似乎也很合理——允许用户以流方式读取数据,节省内存。