Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.917 diff -u -9 -p -r1.917 common.inc --- includes/common.inc 3 Jun 2009 02:50:21 -0000 1.917 +++ includes/common.inc 5 Jun 2009 22:03:06 -0000 @@ -50,18 +50,24 @@ define('JS_LIBRARY', -100); */ define('JS_DEFAULT', 0); /** * The weight of theme JavaScript code being added to the page. */ define('JS_THEME', 100); /** + * Error code indicating that the request made by drupal_http_request() exceeded + * the specified timeout. + */ +define('HTTP_REQUEST_TIMEOUT', 1); + +/** * Add content to a specified region. * * @param $region * Page region the content is added to. * @param $data * Content to be added. */ function drupal_add_region_content($region = NULL, $data = NULL) { static $content = array(); @@ -416,18 +422,22 @@ function drupal_access_denied() { * - headers * An array containing request headers to send as name/value pairs. * - method * A string containing the request method. Defaults to 'GET'. * - data * A string containing the request body. Defaults to NULL. * - max_redirects * An integer representing how many times a redirect may be followed. * Defaults to 3. + * - timeout + * A float representing the maximum number of seconds the function call + * may take. The default is 30 seconds. If a timeout occurs, the error + * code is set to the HTTP_REQUEST_TIMEOUT constant. * @return * An object which can have one or more of the following parameters: * - request * A string containing the request body that was sent. * - code * An integer containing the response status code, or the error code if * an error occurred. * - protocol * The response protocol (e.g. HTTP/1.1 or HTTP/1.0). @@ -456,29 +466,31 @@ function drupal_http_request($url, array $result->error = 'unable to parse URL'; return $result; } if (!isset($uri['scheme'])) { $result->error = 'missing schema'; return $result; } + timer_start(__FUNCTION__); + switch ($uri['scheme']) { case 'http': $port = isset($uri['port']) ? $uri['port'] : 80; $host = $uri['host'] . ($port != 80 ? ':' . $port : ''); - $fp = @fsockopen($uri['host'], $port, $errno, $errstr, 15); + $fp = @fsockopen($uri['host'], $port, $errno, $errstr, $options['timeout']); break; case 'https': // Note: Only works when PHP is compiled with OpenSSL support. $port = isset($uri['port']) ? $uri['port'] : 443; $host = $uri['host'] . ($port != 443 ? ':' . $port : ''); - $fp = @fsockopen('ssl://' . $uri['host'], $port, $errno, $errstr, 20); + $fp = @fsockopen('ssl://' . $uri['host'], $port, $errno, $errstr, $options['timeout']); break; default: $result->error = 'invalid schema ' . $uri['scheme']; return $result; } // Make sure the socket opened properly. if (!$fp) { // When a network error occurs, we use a negative number so it does not @@ -501,18 +513,19 @@ function drupal_http_request($url, array $path .= '?' . $uri['query']; } // Merge the default options. $options += array( 'headers' => array(), 'method' => 'GET', 'data' => NULL, 'max_redirects' => 3, + 'timeout' => 30, ); // Merge the default headers. $options['headers'] += array( 'User-Agent' => 'Drupal (+http://drupal.org/)', ); // RFC 2616: "non-standard ports MUST, default ports MAY be included". // We don't add the standard port to prevent from breaking rewrite rules @@ -547,20 +560,28 @@ function drupal_http_request($url, array $request .= $name . ': ' . trim($value) . "\r\n"; } $request .= "\r\n" . $options['data']; $result->request = $request; fwrite($fp, $request); // Fetch response. $response = ''; - while (!feof($fp) && $chunk = fread($fp, 1024)) { - $response .= $chunk; + while (!feof($fp)) { + // Calculate how much time is left of the original timeout value. + $timeout = $options['timeout'] - timer_read(__FUNCTION__) / 1000; + if ($timeout <= 0) { + $result->code = HTTP_REQUEST_TIMEOUT; + $result->error = 'request timed out'; + return $result; + } + stream_set_timeout($fp, floor($timeout), floor(1000000 * fmod($timeout, 1))); + $response .= fread($fp, 1024); } fclose($fp); // Parse response headers from the response body. list($response, $result->data) = explode("\r\n\r\n", $response, 2); $response = preg_split("/\r\n|\n|\r/", $response); // Parse the response status line. list($protocol, $code, $status_message) = explode(' ', trim(array_shift($response)), 3); @@ -633,19 +654,24 @@ function drupal_http_request($url, array switch ($code) { case 200: // OK case 304: // Not modified break; case 301: // Moved permanently case 302: // Moved temporarily case 307: // Moved temporarily $location = $result->headers['Location']; - if ($options['max_redirects']) { + $options['timeout'] -= timer_read(__FUNCTION__) / 1000; + if ($options['timeout'] <= 0) { + $result->code = HTTP_REQUEST_TIMEOUT; + $result->error = 'request timed out'; + } + elseif ($options['max_redirects']) { // Redirect to the new location. $options['max_redirects']--; $result = drupal_http_request($location, $options); $result->redirect_code = $code; } $result->redirect_url = $location; break; default: $result->error = $status_message; Index: modules/simpletest/tests/common.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/common.test,v retrieving revision 1.44 diff -u -9 -p -r1.44 common.test --- modules/simpletest/tests/common.test 2 Jun 2009 03:33:36 -0000 1.44 +++ modules/simpletest/tests/common.test 5 Jun 2009 22:03:06 -0000 @@ -303,18 +303,29 @@ class DrupalHTTPRequestTestCase extends $this->assertEqual($result->code, 200, t('Fetched page successfully.')); $this->drupalSetContent($result->data); $this->assertTitle(t('Welcome to @site-name | @site-name', array('@site-name' => variable_get('site_name', 'Drupal'))), t('Site title matches.')); // Test that code and status message is returned. $result = drupal_http_request(url('pagedoesnotexist', array('absolute' => TRUE))); $this->assertTrue(!empty($result->protocol), t('Result protocol is returned.')); $this->assertEqual($result->code, '404', t('Result code is 404')); $this->assertEqual($result->status_message, 'Not Found', t('Result status message is "Not Found"')); + + // Test that timeout is respected. The test machine is expected to be able + // to make the connection (i.e. complete the fsockopen()) in 2 seconds and + // return within a total of 10 seconds. If the test machine is extremely + // slow, the test will fail. + $start = timer_start(__METHOD__); + $result = drupal_http_request(url('system-test/sleep/12', array('absolute' => TRUE)), array('timeout' => 2)); + $time = timer_read(__METHOD__) / 1000; + $this->assertTrue($time < 10, t('Request timed out (%time seconds).', array('%time' => $time))); + $this->assertTrue($result->error, t('An error message was returned.')); + $this->assertEqual($result->code, HTTP_REQUEST_TIMEOUT, t('Proper error code was returned.')); } function testDrupalHTTPRequestBasicAuth() { $username = $this->randomName(); $password = $this->randomName(); $url = url('system-test/auth', array('absolute' => TRUE)); $auth = str_replace('http://', 'http://' . $username . ':' . $password . '@', $url); $result = drupal_http_request($auth); Index: modules/simpletest/tests/system_test.module =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/system_test.module,v retrieving revision 1.11 diff -u -9 -p -r1.11 system_test.module --- modules/simpletest/tests/system_test.module 27 May 2009 18:34:00 -0000 1.11 +++ modules/simpletest/tests/system_test.module 5 Jun 2009 22:03:06 -0000 @@ -1,16 +1,22 @@ 'system_test_sleep', + 'page arguments' => array(2), + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); $items['system-test/auth'] = array( 'page callback' => 'system_test_basic_auth_page', 'access callback' => TRUE, 'type' => MENU_CALLBACK, ); $items['system-test/redirect/%'] = array( 'title' => 'Redirect', 'page callback' => 'system_test_redirect', 'page arguments' => array(2), @@ -50,18 +56,22 @@ function system_test_menu() { 'page callback' => 'variable_get', 'page arguments' => array('simpletest_bootstrap_variable_test', NULL), 'access arguments' => array('access content'), 'type' => MENU_CALLBACK, ); return $items; } +function system_test_sleep($seconds) { + sleep($seconds); +} + function system_test_basic_auth_page() { $output = t('$_SERVER[\'PHP_AUTH_USER\'] is @username.', array('@username' => $_SERVER['PHP_AUTH_USER'])); $output .= t('$_SERVER[\'PHP_AUTH_PW\'] is @password.', array('@password' => $_SERVER['PHP_AUTH_PW'])); return $output; } function system_test_redirect($code) { $code = (int)$code; if ($code != 200) {