PHP: Making file_get_contents() more routing-robust and dual-stack

file_get_contents() is the preferred way to read the contents of a file into a string, according to PHP documentation. Unfortunately, it isn’t magic and it needs some work to make it resilient to networking problems.

I noticed in my logs that some remote endpoints and even my own server have short periods where either IPv6 or IPv4 is unroutable. In almost all instances the other protocol version has continued to work while the other has been unroutable. A proper dual-stack application would try both protocol versions if there’s a problem with either protocols.

While the file_get_contents() function sure is a convenient method for fetching files over a multitude of network protocols, it doesn’t make any efforts to recover if it runs into any kind of networking problems. The function call only makes one connection attempt against the first DNS answer as decided by your DNS client.

This can be improved by implementing a wrapper around file_get_contents() that binds a socket to the unspecified address for the appropriate protocol version, and tries the other protocol version if the first fails. By binding the socket to the special “unspecified address” rather than an actual local IP address, you effectively bind the socket to a protocol version family; thus forcing the underlying operating system to only use IPv6 or IPv4. (The unspecified socket address for IPv6 is “[0]:0” and “0:0” for IPv4.)

To avoid trying to use an unsupported protocol version if either the local or remote system doesn’t support a given IP version, you can ask the local DNS server for the IPv6 and IPv4 address of the remote host. You’ll likely not be making any additional DNS requests because of this, as your server or networking infrastructure [most likely] will already have cached the repose. (This assumes you’ll not get any AAAA responses if you’re no an IPv4-only network.)

Following this logic (an example implementation provided below), you should have a more robust implementation that will be more fault-tolerant of temporary routing issues. The code examples implements the points discussed above as a fetch_http_file_contents() as a more robust replacement for file_get_contents() for use when fetching targets over the Internet.

# SPDX-License-Identifier: CC0-1.0

function fetch_http_file_contents($url) {
  $hostname = parse_url($url, PHP_URL_HOST);
  if ($hostname == FALSE) {
    return FALSE;
  }

  $host_has_ipv6 = FALSE;
  $host_has_ipv4 = FALSE;
  $file_response = FALSE;

  $dns_records = dns_get_record($hostname, DNS_AAAA + DNS_A);

  foreach ($dns_records as $dns_record) {
    if (isset($dns_record['type'])) {
      switch ($dns_record['type']) {
        case 'AAAA':
          $host_has_ipv6 = TRUE;
          break;
        case 'A':
          $host_has_ipv4 = TRUE;
          break;
  } } }

  if ($host_has_ipv6 === TRUE) {
    $file_response = file_get_intbound_contents($url, '[0]:0');
  }
  if ($host_has_ipv4 === TRUE && $file_response == FALSE) {
    $file_response = file_get_intbound_contents($url, '0:0');
  }

  return $file_response;
}

function file_get_intbound_contents($url, $bindto_addr_family) {
  $stream_context = stream_context_create(
                      array(
                        'socket' => array(
                          'bindto' => $bindto_addr_family
                        ),
                        'http' => array(
                          'timeout'=>20,
                          'method'=>'GET'
                    ) ) );

  return file_get_contents($url, FALSE, $stream_context);
}

Depending on the exact network conditions on your local server and the remote endpoint, the above can be a solution to the following two PHP warnings:

file_get_contents(): failed to open stream: No route to host
file_get_contents(): failed to open stream: Connection timed out

I just wanted to offer this as an alternative to the many “just disable IPv6!” responses people usually get when someone asks about routing problems and file_get_contents() in coding forums and on Stack Overflow. Alternatively, people suggest to use cURL instead of native PHP code, but this introduces an additional external dependency.

The above code examples by © 2016 Daniel Aleksandersen. The code example is licensed under the permissive MIT License. The source photo for the feature image by © 2016 William Hook.