diff --git a/src/Requests.php b/src/Requests.php index b78b93420..d8f133af8 100644 --- a/src/Requests.php +++ b/src/Requests.php @@ -749,7 +749,7 @@ protected static function parse_response($headers, $url, $req_headers, $req_data // Unfold headers (replace [CRLF] 1*( SP | HT ) with SP) as per RFC 2616 (section 2.2) $headers = preg_replace('/\n[ \t]/', ' ', $headers); $headers = explode("\n", $headers); - preg_match('#^HTTP/(1\.\d)[ \t]+(\d+)#i', array_shift($headers), $matches); + preg_match('#^HTTP/(1\.\d|2|3)[ \t]+(\d+)#i', array_shift($headers), $matches); if (empty($matches)) { throw new Exception('Response could not be parsed', 'noversion', $headers); } diff --git a/src/Transport/Curl.php b/src/Transport/Curl.php index 18de09d2a..4e025b494 100644 --- a/src/Transport/Curl.php +++ b/src/Transport/Curl.php @@ -393,11 +393,11 @@ private function setup_handle($url, $headers, $data, $options) { * 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. + * a protocol version newer than HTTP/1.0. * * https://curl.se/mail/lib-2017-07/0013.html */ - if (!isset($headers['Expect']) && $options['protocol_version'] === 1.1) { + if (!isset($headers['Expect']) && $options['protocol_version'] > 1.0) { $headers['Expect'] = $this->get_expect_header($data); } @@ -465,10 +465,19 @@ private function setup_handle($url, $headers, $data, $options) { curl_setopt($this->handle, CURLOPT_HTTPHEADER, $headers); } - if ($options['protocol_version'] === 1.1) { - curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); - } else { - curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); + switch ($options['protocol_version']) { + case 3.0: + // The CURL_HTTP_VERSION_3 constant is only available from PHP 8.4 + curl_setopt($this->handle, CURLOPT_HTTP_VERSION, 30); + break; + case 2.0: + curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0); + break; + case 1.1: + curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + break; + default: + curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); } if ($options['blocking'] === true) { diff --git a/tests/Requests/RequestsTest.php b/tests/Requests/RequestsTest.php index e229ace17..e7be82935 100644 --- a/tests/Requests/RequestsTest.php +++ b/tests/Requests/RequestsTest.php @@ -190,10 +190,27 @@ public function testHeaderParsing() { } } - public function testProtocolVersionParsing() { + /** + * Data Provider. + * + * @return array + */ + public function dataProtocolVersion() { + return [ + 'HTTP/1.0' => ['1.0', 1.0], + 'HTTP/1.1' => ['1.1', 1.1], + 'HTTP/2' => ['2', 2.0], + 'HTTP/3' => ['3', 3.0], + ]; + } + + /** + * @dataProvider dataProtocolVersion + */ + public function testProtocolVersionParsing($version, $expected) { $transport = new RawTransportMock(); $transport->data = - "HTTP/1.0 200 OK\r\n" . + "HTTP/$version 200 OK\r\n" . "Host: localhost\r\n\r\n"; $options = [ @@ -201,7 +218,7 @@ public function testProtocolVersionParsing() { ]; $response = Requests::get('http://example.com/', [], $options); - $this->assertSame(1.0, $response->protocol_version); + $this->assertSame($expected, $response->protocol_version); } public function testRawAccess() { diff --git a/tests/Transport/Curl/CurlTest.php b/tests/Transport/Curl/CurlTest.php index b7a2442d4..871f04404 100644 --- a/tests/Transport/Curl/CurlTest.php +++ b/tests/Transport/Curl/CurlTest.php @@ -54,7 +54,7 @@ public function testDoesntOverwriteExpectHeaderIfManuallySet() { /** * @small */ - public function testDoesntSetExpectHeaderIfBodyExactly1MbButProtocolIsnt11() { + public function testDoesNotSetEmptyExpectHeaderIfBodyExactly1MbAndProtocolIs10() { $options = [ 'protocol_version' => 1.0, ]; @@ -68,7 +68,37 @@ public function testDoesntSetExpectHeaderIfBodyExactly1MbButProtocolIsnt11() { /** * @small */ - public function testSetsEmptyExpectHeaderWithDefaultSettings() { + public function testSetsEmptyExpectHeaderIfBodyExactly1MbAndProtocolIs20() { + $this->markTestSkipped('HTTP/2 send fails with: cURL error 55: Send failure: Broken pipe'); + $options = [ + 'protocol_version' => 2.0, + ]; + $request = Requests::post($this->httpbin('/post'), [], str_repeat('x', 1048576), $this->getOptions($options)); + + $result = json_decode($request->body, true); + + $this->assertSame($result['headers']['Expect'], ''); + } + + /** + * @small + */ + public function testSetsEmptyExpectHeaderIfBodyExactly1MbAndProtocolIs30() { + $this->markTestSkipped('HTTP/3 connection times out'); + $options = [ + 'protocol_version' => 3.0, + ]; + $request = Requests::post($this->httpbin('/post', true), [], str_repeat('x', 1048576), $this->getOptions($options)); + + $result = json_decode($request->body, true); + + $this->assertSame($result['headers']['Expect'], ''); + } + + /** + * @small + */ + public function testDoesNotSetEmptyExpectHeaderWithDefaultSettings() { $request = Requests::post($this->httpbin('/post'), [], [], $this->getOptions()); $result = json_decode($request->body, true);