I am currently implementing a PHP class that fetches image files and caches them locally. These images may come from other local sources, via HTTP or via HTTP using the Guzzle client. With PHP stream wrappers I should be able to handle all sources the same way.
What I am now trying to do ist to implement a timeout if no data is transferred through the stream. This should handle the following cases:
fopen
call and not with a timeout.I think I can do all this with stream_set_timeout
but it isn't quite clear to me what this actually does. Does the timeout apply if any operation on the stream takes longer than allowed, i.e. I can do something that takes 0.5 s twice with a timeout of 0.75 s? Or does it only apply if no data is transferred through the stream for longer than the allowed time?
I tried to test the behavior with this short script:
<?php
$in = fopen('https://reqres.in/api/users?delay=5', 'r');
$out = fopen('out', 'w');
stream_set_timeout($in, 1);
stream_copy_to_stream($in, $out);
var_dump(stream_get_meta_data($in)['timed_out']);
Although the response from reqres.in
is delayed 5 s I always get false
with a timeout of 1 s. Please can somebody explain this?
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 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.
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.
- 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.
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.
There is a difference between "read time out" and "connection time out"..
The connection timeout is the timeout in making the initial connection ( completing the TCP connection handshake). The read timeout is the timeout on waiting to read data. If the server does not send a byte XX seconds after the last byte, a read timeout error is generated.
Even if you saw a delay (response time) of 5s - this happens probably during the initial connect (DNS lookup, connect etc) and not during your read.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With