From a7ecaf43eea5a4060b65feb788bb6b704dbb9205 Mon Sep 17 00:00:00 2001 From: Heinz Wiesinger Date: Mon, 10 Jun 2024 09:19:56 +0200 Subject: [PATCH] Add support for HTTP2 and HTTP3 --- src/Requests.php | 2 +- src/Transport/Curl.php | 21 +++++++---- tests/RequestsTest.php | 23 ++++++++++-- tests/Transport/CurlTest.php | 69 +++++++++++++++++++++++++++++------- 4 files changed, 92 insertions(+), 23 deletions(-) diff --git a/src/Requests.php b/src/Requests.php index 754745d3d..fd890591e 100644 --- a/src/Requests.php +++ b/src/Requests.php @@ -772,7 +772,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 41845386c..a99a495d0 100644 --- a/src/Transport/Curl.php +++ b/src/Transport/Curl.php @@ -374,11 +374,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. + * 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.1) { $headers['Expect'] = $this->get_expect_header($data); } @@ -446,10 +446,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/RequestsTest.php b/tests/RequestsTest.php index 856c3d514..30e831f32 100644 --- a/tests/RequestsTest.php +++ b/tests/RequestsTest.php @@ -55,6 +55,20 @@ public function dataRequestInvalidUrl() { ]; } + /** + * Data Provider. + * + * @return array + */ + public function dataProtocolVerison() { + 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], + ]; + } + /** * Tests receiving an exception when the request() method received an invalid input type as `$type`. * @@ -287,10 +301,13 @@ public function testHeaderParsing() { } } - public function testProtocolVersionParsing() { + /** + * @dataProvider dataProtocolVerison + */ + 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 = [ @@ -298,7 +315,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/CurlTest.php b/tests/Transport/CurlTest.php index 0c3211e6d..b29fb549a 100644 --- a/tests/Transport/CurlTest.php +++ b/tests/Transport/CurlTest.php @@ -55,9 +55,37 @@ public function testDoesntOverwriteExpectHeaderIfManuallySet() { /** * @small */ - public function testDoesntSetExpectHeaderIfBodyExactly1MbButProtocolIsnt11() { + public function testDoesNotSetEmptyExpectHeaderIfBodyExactly1MbAndProtocolIs11() { $options = [ - 'protocol_version' => 1.0, + 'protocol_version' => 1.1, + ]; + $request = Requests::post($this->httpbin('/post'), [], str_repeat('x', 1048576), $this->getOptions($options)); + + $result = json_decode($request->body, true); + + $this->assertFalse(isset($result['headers']['Expect'])); + } + + /** + * @small + */ + public function testDoesNotSetEmptyExpectHeaderIfBodyExactly1MbAndProtocolIs20() { + $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->assertFalse(isset($result['headers']['Expect'])); + } + + /** + * @small + */ + public function testDoesNotSetEmptyExpectHeaderIfBodyExactly1MbAndProtocolIs30() { + $options = [ + 'protocol_version' => 3.0, ]; $request = Requests::post($this->httpbin('/post'), [], str_repeat('x', 1048576), $this->getOptions($options)); @@ -69,7 +97,7 @@ public function testDoesntSetExpectHeaderIfBodyExactly1MbButProtocolIsnt11() { /** * @small */ - public function testSetsEmptyExpectHeaderWithDefaultSettings() { + public function testDoesNotSetsEmptyExpectHeaderWithDefaultSettings() { $request = Requests::post($this->httpbin('/post'), [], [], $this->getOptions()); $result = json_decode($request->body, true); @@ -80,29 +108,38 @@ public function testSetsEmptyExpectHeaderWithDefaultSettings() { /** * @small */ - public function testSetsEmptyExpectHeaderIfBodyIsANestedArrayLessThan1Mb() { + public function testSetsEmptyExpectHeaderIfBodyIsANestedArrayLessThan1MbAndProtocolIs10() { + $options = [ + 'protocol_version' => 1.0, + ]; $data = [ str_repeat('x', 148576), [ str_repeat('x', 548576), ], ]; - $request = Requests::post($this->httpbin('/post'), [], $data, $this->getOptions()); + $request = Requests::post($this->httpbin('/post'), [], $data, $this->getOptions($options)); $result = json_decode($request->body, true); $this->assertFalse(isset($result['headers']['Expect'])); } - public function testSetsExpectHeaderIfBodyIsExactlyA1MbString() { - $request = Requests::post($this->httpbin('/post'), [], str_repeat('x', 1048576), $this->getOptions()); + public function testSetsExpectHeaderIfBodyIsExactlyA1MbStringAndProtocolIs10() { + $options = [ + 'protocol_version' => 1.0, + ]; + $request = Requests::post($this->httpbin('/post'), [], str_repeat('x', 1048576), $this->getOptions($options)); $result = json_decode($request->body, true); $this->assertSame('100-Continue', $result['headers']['Expect']); } - public function testSetsExpectHeaderIfBodyIsANestedArrayGreaterThan1Mb() { + public function testSetsExpectHeaderIfBodyIsANestedArrayGreaterThan1MbAndProtocolIs10() { + $options = [ + 'protocol_version' => 1.0, + ]; $data = [ str_repeat('x', 148576), [ @@ -112,15 +149,18 @@ public function testSetsExpectHeaderIfBodyIsANestedArrayGreaterThan1Mb() { ], ], ]; - $request = Requests::post($this->httpbin('/post'), [], $data, $this->getOptions()); + $request = Requests::post($this->httpbin('/post'), [], $data, $this->getOptions($options)); $result = json_decode($request->body, true); $this->assertSame('100-Continue', $result['headers']['Expect']); } - public function testSetsExpectHeaderIfBodyExactly1Mb() { - $request = Requests::post($this->httpbin('/post'), [], str_repeat('x', 1048576), $this->getOptions()); + public function testSetsExpectHeaderIfBodyExactly1MbAndProtocolIs10() { + $options = [ + 'protocol_version' => 1.0, + ]; + $request = Requests::post($this->httpbin('/post'), [], str_repeat('x', 1048576), $this->getOptions($options)); $result = json_decode($request->body, true); @@ -130,8 +170,11 @@ public function testSetsExpectHeaderIfBodyExactly1Mb() { /** * @small */ - public function testSetsEmptyExpectHeaderIfBodySmallerThan1Mb() { - $request = Requests::post($this->httpbin('/post'), [], str_repeat('x', 1048575), $this->getOptions()); + public function testSetsEmptyExpectHeaderIfBodySmallerThan1MbAndProtocolIs10() { + $options = [ + 'protocol_version' => 1.0, + ]; + $request = Requests::post($this->httpbin('/post'), [], str_repeat('x', 1048575), $this->getOptions($options)); $result = json_decode($request->body, true);