Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PHP stream timeout if no data is transferred

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:

  1. The stream cannot be established in the first place. This should probably be handled at the fopen call and not with a timeout.
  2. The stream is established but no data is transferred.
  3. 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?

like image 879
Mouagip Avatar asked Jan 28 '23 14:01

Mouagip


2 Answers

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

  1. 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.

  1. 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).

  1. 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.

like image 83
knownunown Avatar answered Feb 11 '23 22:02

knownunown


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.

like image 45
michael - mlc Avatar answered Feb 11 '23 22:02

michael - mlc