diff --git a/integration/tests_failed/http_version_not_supported.err b/integration/tests_failed/http_version_not_supported.err new file mode 100644 index 00000000000..4c08f38f3d7 --- /dev/null +++ b/integration/tests_failed/http_version_not_supported.err @@ -0,0 +1,7 @@ +error: Unsupported HTTP version + --> tests_failed/http_version_not_supported.hurl:3:5 + | + 3 | GET http://localhost:8000/foo + | ^^^^^^^^^^^^^^^^^^^^^^^^^ HTTP/3 is not supported, check --version + | + diff --git a/integration/tests_failed/http_version_not_supported.exit b/integration/tests_failed/http_version_not_supported.exit new file mode 100644 index 00000000000..00750edc07d --- /dev/null +++ b/integration/tests_failed/http_version_not_supported.exit @@ -0,0 +1 @@ +3 diff --git a/integration/tests_failed/http_version_not_supported.html b/integration/tests_failed/http_version_not_supported.html new file mode 100644 index 00000000000..7b9652c7459 --- /dev/null +++ b/integration/tests_failed/http_version_not_supported.html @@ -0,0 +1,5 @@ +
# This test is run when the libcurl used by Hurl doesn't support HTTP/3 so it should failed with
+# an appropriate error message.
+GET http://localhost:8000/foo
+HTTP 200
+
diff --git a/integration/tests_failed/http_version_not_supported.hurl b/integration/tests_failed/http_version_not_supported.hurl new file mode 100644 index 00000000000..f7b80d53f26 --- /dev/null +++ b/integration/tests_failed/http_version_not_supported.hurl @@ -0,0 +1,4 @@ +# This test is run when the libcurl used by Hurl doesn't support HTTP/3 so it should failed with +# an appropriate error message. +GET http://localhost:8000/foo +HTTP 200 diff --git a/integration/tests_failed/http_version_not_supported.json b/integration/tests_failed/http_version_not_supported.json new file mode 100644 index 00000000000..3d348dd0ba6 --- /dev/null +++ b/integration/tests_failed/http_version_not_supported.json @@ -0,0 +1 @@ +{"entries":[{"request":{"method":"GET","url":"http://localhost:8000/foo"},"response":{"status":200}}]} diff --git a/integration/tests_failed/http_version_not_supported.ps1 b/integration/tests_failed/http_version_not_supported.ps1 new file mode 100644 index 00000000000..0aa09a25fd9 --- /dev/null +++ b/integration/tests_failed/http_version_not_supported.ps1 @@ -0,0 +1,11 @@ +Set-StrictMode -Version latest +$ErrorActionPreference = 'Stop' + +$ErrorActionPreference = 'Continue' +curl --version | grep Features | grep -q HTTP3 +if ($LASTEXITCODE -eq 0) { + exit 255 +} +$ErrorActionPreference = 'Stop' + +hurl --http3 tests_failed/http_version_not_supported.hurl diff --git a/integration/tests_failed/http_version_not_supported.sh b/integration/tests_failed/http_version_not_supported.sh new file mode 100755 index 00000000000..36c210a07df --- /dev/null +++ b/integration/tests_failed/http_version_not_supported.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -Eeuo pipefail + +set +eo pipefail +if (curl --version | grep Features | grep -q HTTP3); then + exit 255 +fi +set -Eeuo pipefail + +hurl --http3 tests_failed/http_version_not_supported.hurl diff --git a/packages/hurl/src/http/client.rs b/packages/hurl/src/http/client.rs index de9a5d3046c..40bdc18a952 100644 --- a/packages/hurl/src/http/client.rs +++ b/packages/hurl/src/http/client.rs @@ -21,8 +21,8 @@ use std::str::FromStr; use base64::engine::general_purpose; use base64::Engine; use chrono::Utc; -use curl::easy; use curl::easy::{List, SslOpt}; +use curl::{easy, Version}; use encoding::all::ISO_8859_1; use encoding::{DecoderTrap, Encoding}; use url::Url; @@ -48,6 +48,9 @@ pub struct Client { handle: Box, /// Current State state: ClientState, + /// HTTP version support + http2: bool, + http3: bool, } /// Represents the state of the HTTP client. @@ -78,10 +81,13 @@ impl ClientState { impl Client { /// Creates HTTP Hurl client. pub fn new() -> Client { - let h = easy::Easy::new(); + let handle = easy::Easy::new(); + let version = Version::get(); Client { - handle: Box::new(h), + handle: Box::new(handle), state: ClientState::default(), + http2: version.feature_http2(), + http3: version.feature_http3(), } } @@ -157,6 +163,30 @@ impl Client { // way to get access to the outgoing headers. self.handle.verbose(true)?; + // We checks libcurl HTTP version support. + let http_version = options.http_version; + if (http_version == RequestedHttpVersion::Http2 && !self.http2) + || (http_version == RequestedHttpVersion::Http3 && !self.http3) + { + return Err(HttpError::UnsupportedHttpVersion(http_version)); + } + + // libcurl tries to reuse connections as much as possible (see ) + // That's why an `handle` initiated with a HTTP 2 version may keep using HTTP 2 protocol + // even if we ask to switch to HTTP 3 in the same session (using `[Options]` section for + // instance). + // > Note that the HTTP version is just a request. libcurl still prioritizes to reuse + // > existing connections so it might then reuse a connection using a HTTP version you + // > have not asked for. + // + // So, if we detect a change of requested HTTP version, we force libcurl to refresh its + // connections (see ) + self.state.set_requested_http_version(http_version); + if self.state.has_changed() { + logger.debug("Force refreshing connections because requested HTTP version change"); + self.handle.fresh_connect(true)?; + } + // Activates the access of certificates info chain after a transfer has been executed. self.handle.certinfo(true)?; @@ -192,23 +222,6 @@ impl Client { self.handle.timeout(options.timeout)?; self.handle.connect_timeout(options.connect_timeout)?; - // libcurl tries to reuse connections as much as possible (see ) - // That's why an `handle` initiated with a HTTP 2 version may keep using HTTP 2 protocol - // even if we ask to switch to HTTP 3 in the same session (using `[Options]` section for - // instance). - // > Note that the HTTP version is just a request. libcurl still prioritizes to reuse - // > existing connections so it might then reuse a connection using a HTTP version you - // > have not asked for. - // - // So, if we detect a change of requested HTTP version, we force libcurl to refresh its - // connections (see ) - self.state.set_requested_http_version(options.http_version); - if self.state.has_changed() { - logger.debug("Force refreshing connections because requested HTTP version change"); - self.handle.fresh_connect(true)?; - } - self.handle.http_version(options.http_version.into())?; - self.set_ssl_options(options.ssl_no_revoke)?; let url = self.generate_url(&request_spec.url, &request_spec.querystring); diff --git a/packages/hurl/src/http/error.rs b/packages/hurl/src/http/error.rs index ce358ac4974..d421ea081e3 100644 --- a/packages/hurl/src/http/error.rs +++ b/packages/hurl/src/http/error.rs @@ -16,6 +16,8 @@ * */ +use crate::http::RequestedHttpVersion; + #[derive(Clone, Debug, PartialEq, Eq)] pub enum HttpError { CouldNotParseResponse, @@ -45,6 +47,7 @@ pub enum HttpError { UnsupportedContentEncoding { description: String, }, + UnsupportedHttpVersion(RequestedHttpVersion), InvalidUrl(String), InvalidUrlPrefix(String), } diff --git a/packages/hurl/src/http/request.rs b/packages/hurl/src/http/request.rs index 67667b6f32f..e30927aee49 100644 --- a/packages/hurl/src/http/request.rs +++ b/packages/hurl/src/http/request.rs @@ -15,6 +15,7 @@ * limitations under the License. * */ +use std::fmt; use url::Url; use crate::http::core::*; @@ -38,6 +39,19 @@ pub enum RequestedHttpVersion { Http3, } +impl fmt::Display for RequestedHttpVersion { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let value = match self { + RequestedHttpVersion::Default => "HTTP (default)", + RequestedHttpVersion::Http10 => "HTTP/1.0", + RequestedHttpVersion::Http11 => "HTTP/1.1", + RequestedHttpVersion::Http2 => "HTTP/2", + RequestedHttpVersion::Http3 => "HTTP/3", + }; + write!(f, "{value}") + } +} + impl Request { /// Extracts query string params from the url of the request. pub fn query_string_params(&self) -> Vec { diff --git a/packages/hurl/src/runner/core.rs b/packages/hurl/src/runner/core.rs index 391506f737f..f374d0cfe0e 100644 --- a/packages/hurl/src/runner/core.rs +++ b/packages/hurl/src/runner/core.rs @@ -19,7 +19,7 @@ use std::path::PathBuf; use hurl_core::ast::SourceInfo; -use crate::http::{Call, Cookie}; +use crate::http::{Call, Cookie, RequestedHttpVersion}; use crate::runner::value::Value; #[derive(Clone, Debug, PartialEq, Eq)] @@ -132,6 +132,7 @@ pub enum RunnerError { SslCertificate(String), UnsupportedContentEncoding(String), + UnsupportedHttpVersion(RequestedHttpVersion), CouldNotUncompressResponse(String), FileReadAccess { diff --git a/packages/hurl/src/runner/error.rs b/packages/hurl/src/runner/error.rs index a72b9ab95f5..3d01580a3bf 100644 --- a/packages/hurl/src/runner/error.rs +++ b/packages/hurl/src/runner/error.rs @@ -61,6 +61,7 @@ impl Error for runner::Error { RunnerError::UnrenderableVariable { .. } => "Unrenderable variable".to_string(), RunnerError::NoQueryResult => "No query result".to_string(), RunnerError::UnsupportedContentEncoding(..) => "Decompression error".to_string(), + RunnerError::UnsupportedHttpVersion(..) => "Unsupported HTTP version".to_string(), RunnerError::CouldNotUncompressResponse(..) => "Decompression error".to_string(), RunnerError::InvalidJson { .. } => "Invalid JSON".to_string(), RunnerError::UnauthorizedFileAccess { .. } => "Unauthorized file access".to_string(), @@ -143,6 +144,10 @@ impl Error for runner::Error { RunnerError::UnsupportedContentEncoding(algorithm) => { format!("compression {algorithm} is not supported") } + RunnerError::UnsupportedHttpVersion(version) => { + format!("{version} is not supported, check --version") + } + RunnerError::CouldNotUncompressResponse(algorithm) => { format!("could not uncompress response with {algorithm}") } @@ -206,6 +211,9 @@ impl From for RunnerError { HttpError::UnsupportedContentEncoding { description } => { RunnerError::UnsupportedContentEncoding(description) } + HttpError::UnsupportedHttpVersion(version) => { + RunnerError::UnsupportedHttpVersion(version) + } HttpError::InvalidUrl(url) => RunnerError::InvalidUrl(url), HttpError::InvalidUrlPrefix(url) => RunnerError::InvalidUrlPrefix(url), }