diff --git a/library/Requests/Transport/cURL.php b/library/Requests/Transport/cURL.php index 2453de1db..01bcf3b71 100644 --- a/library/Requests/Transport/cURL.php +++ b/library/Requests/Transport/cURL.php @@ -317,6 +317,21 @@ protected function setup_handle($url, $headers, $data, $options) { $headers['Connection'] = 'close'; } + /** + * Add "Expect" header. + * + * By default, cURL adds a "Expect: 100-Continue" to most requests. This header can + * add as much as a second to the time it takes for cURL to perform a request. To + * prevent this, we need to set an empty "Expect" header. To match the behaviour of + * Guzzle, we'll add the empty header to requests that are smaller than 1 MB and use + * HTTP/1.1. + * + * https://curl.se/mail/lib-2017-07/0013.html + */ + if (!isset($headers['Expect']) && $options['protocol_version'] === 1.1) { + $headers['Expect'] = $this->get_expect_header($data); + } + $headers = Requests::flatten($headers); if (!empty($data)) { @@ -546,4 +561,29 @@ public static function test($capabilities = array()) { return true; } + + /** + * Get the correct "Expect" header for the given request data. + * + * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD. + * @return string The "Expect" header. + */ + protected function get_expect_header($data) { + if (!is_array($data)) { + return strlen((string) $data) >= 1048576 ? '100-Continue' : ''; + } + + $bytesize = 0; + $iterator = new RecursiveIteratorIterator(new RecursiveArrayIterator($data)); + + foreach ($iterator as $datum) { + $bytesize += strlen((string) $datum); + + if ($bytesize >= 1048576) { + return '100-Continue'; + } + } + + return ''; + } } diff --git a/tests/Transport/cURL.php b/tests/Transport/cURL.php index d0095228c..0690d7b4b 100644 --- a/tests/Transport/cURL.php +++ b/tests/Transport/cURL.php @@ -36,4 +36,104 @@ public function testRevokedHTTPS() { public function testBadDomain() { parent::testBadDomain(); } + + /** + * @small + */ + public function testDoesntOverwriteExpectHeaderIfManuallySet() { + $headers = array( + 'Expect' => 'foo', + ); + $request = Requests::post(httpbin('/post'), $headers, array(), $this->getOptions()); + + $result = json_decode($request->body, true); + + $this->assertSame($headers['Expect'], $result['headers']['Expect']); + } + + /** + * @small + */ + public function testDoesntSetExpectHeaderIfBodyExactly1MbButProtocolIsnt11() { + $options = array( + 'protocol_version' => 1.0, + ); + $request = Requests::post(httpbin('/post'), array(), str_repeat('x', 1048576), $this->getOptions($options)); + + $result = json_decode($request->body, true); + + $this->assertFalse(isset($result['headers']['Expect'])); + } + + /** + * @small + */ + public function testSetsEmptyExpectHeaderWithDefaultSettings() { + $request = Requests::post(httpbin('/post'), array(), array(), $this->getOptions()); + + $result = json_decode($request->body, true); + + $this->assertFalse(isset($result['headers']['Expect'])); + } + + /** + * @small + */ + public function testSetsEmptyExpectHeaderIfBodyIsANestedArrayLessThan1Mb() { + $data = array( + str_repeat('x', 148576), + array( + str_repeat('x', 548576), + ), + ); + $request = Requests::post(httpbin('/post'), array(), $data, $this->getOptions()); + + $result = json_decode($request->body, true); + + $this->assertFalse(isset($result['headers']['Expect'])); + } + + public function testSetsExpectHeaderIfBodyIsExactlyA1MbString() { + $request = Requests::post(httpbin('/post'), array(), str_repeat('x', 1048576), $this->getOptions()); + + $result = json_decode($request->body, true); + + $this->assertSame('100-Continue', $result['headers']['Expect']); + } + + public function testSetsExpectHeaderIfBodyIsANestedArrayGreaterThan1Mb() { + $data = array( + str_repeat('x', 148576), + array( + str_repeat('x', 548576), + array( + str_repeat('x', 648576), + ), + ), + ); + $request = Requests::post(httpbin('/post'), array(), $data, $this->getOptions()); + + $result = json_decode($request->body, true); + + $this->assertSame('100-Continue', $result['headers']['Expect']); + } + + public function testSetsExpectHeaderIfBodyExactly1Mb() { + $request = Requests::post(httpbin('/post'), array(), str_repeat('x', 1048576), $this->getOptions()); + + $result = json_decode($request->body, true); + + $this->assertSame('100-Continue', $result['headers']['Expect']); + } + + /** + * @small + */ + public function testSetsEmptyExpectHeaderIfBodySmallerThan1Mb() { + $request = Requests::post(httpbin('/post'), array(), str_repeat('x', 1048575), $this->getOptions()); + + $result = json_decode($request->body, true); + + $this->assertFalse(isset($result['headers']['Expect'])); + } }