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:
- The stream cannot be established in the first place. This should probably be handled at the
fopencall and not with a timeout. - The stream is established but no data is transferred.
- The stream is established, data is transferred but it stops some time during transfer.
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_contentsandfile_put_contentsinstead of streams, they support all the wrappers and you can pass contexts to them like you can tofopen. 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
fopenworks with thehttpstream wrapper (which I didn't fully understand either until I tried it out) in blocking mode. For GET (the default),fopenseems to perform the HTTP request at the time of the call, not at the time the stream is read. This would explain whystream_set_timeoutdoes not function as expected, as it modifies stream context afterfopenis called.The Solution
Thankfully, there is a way to modify the timeout before
fopenis called, rather; you can callfopenwith a context. Passing the context returned fromstream_context_create(as Sammitch linked) tofopentimeouts correctly for all three of your cases. For reference, this is how your script would be modified: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
fopenis called. Now to test your three conditions.Validating behavior
This works properly as is -- if the connection can't be established (server offline, etc), the
fopencall 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,fopenreturns false. You'll have to check for that in your code to avoid using false as a stream.This scenario works as well, just run the script with your normal URL. This also makes
fopenreturn false and trigger a warning (a different one).This is an interesting case. To test this, you can write a script that sends the
Content-Lengthand some other headers along with some partial data, then wait until the timeout, i.e.:The
ob_flushis necessary to make PHP write the output (without closing the connection) before the sleep and the script exit. You can serve this usingphp -S localhost:portthen point the other script tolocalhost:port. The client script in this case does not throw a warning andfopenactually returns a stream withtimed_outin the metadata set to true.Conclusion
stream_set_timeoutdoes not work with HTTP GET requests andfopenin blocking mode becausefopenexecutes the request when it's called instead of waiting for a read to do so. You can pass a context tofopenwith the timeout to fix this.