I would recommend you use file_get_contents
and file_put_contents
instead of streams, they support all the wrappers and you can pass contexts to them like you can to fopen
. They're a lot easier to use in general since they return and accept strings instead of streams. That being said, I don't know the nature of your caching mechanism and if streams are better for your use case, more power to you :)
The Problem
The problem here seems to be a misunderstanding of how fopen
works with the http
stream wrapper (which I didn't fully understand either until I tried it out) in blocking mode. For GET (the default), fopen
seems to perform the HTTP request at the time of the call, not at the time the stream is read. This would explain why stream_set_timeout
does not function as expected, as it modifies stream context after fopen
is called.
The Solution
Thankfully, there is a way to modify the timeout before fopen
is called, rather; you can call fopen
with a context. Passing the context returned from stream_context_create
(as Sammitch linked) to fopen
timeouts correctly for all three of your cases. For reference, this is how your script would be modified:
<?php
$ctx = stream_context_create(['http' => [
'timeout' => 1.0,
]]);
$in = fopen('https://reqres.in/api/users?delay=5', 'r', false, $ctx);
$out = STDOUT;
stream_copy_to_stream($in, $out);
var_dump(stream_get_meta_data($in)['timed_out']);
fclose($in);
Note: I assumed you meant to copy the stream to stdout instead of "out", which isn't a valid stream on my platform (Darwin). I also fclosed the in stream at the end of the script, which is always good practice.
This would create a stream with a timeout of 1, starting when fopen
is called. Now to test your three conditions.
Validating behavior
- The stream cannot be established in the first place. This should probably be handled at the fopen call and not with a timeout.
This works properly as is -- if the connection can't be established (server offline, etc), the fopen
call triggers a warning immediately. Just point the script at some arbitrary port on localhost that nothing is listening on. Do note that if the connection wasn't successfully established, fopen
returns false. You'll have to check for that in your code to avoid using false as a stream.
- The stream is established but no data is transferred.
This scenario works as well, just run the script with your normal URL. This also makes fopen
return false and trigger a warning (a different one).
- The stream is established, data is transferred but it stops some time during transfer.
This is an interesting case. To test this, you can write a script that sends the Content-Length
and some other headers along with some partial data, then wait until the timeout, i.e.:
<?php
header('Content-Type: text/plain');
header('Content-Length: 10');
echo "hi";
ob_flush();
sleep(10);
The ob_flush
is necessary to make PHP write the output (without closing the connection) before the sleep and the script exit. You can serve this using php -S localhost:port
then point the other script to localhost:port
. The client script in this case does not throw a warning and fopen
actually returns a stream with timed_out
in the metadata set to true.
Conclusion
stream_set_timeout
does not work with HTTP GET requests and fopen
in blocking mode because fopen
executes the request when it's called instead of waiting for a read to do so. You can pass a context to fopen
with the timeout to fix this.