Add timeout option (--connect-timeout and --max-time)

This commit is contained in:
Fabrice Reix 2020-09-23 21:27:49 +02:00
parent 7537d11cb8
commit 4940333bf7
16 changed files with 378 additions and 169 deletions

View File

@ -133,6 +133,12 @@ Read cookies from file (using the Netscape cookie file format).
Combined with \fI-c, --cookie-jar\fP, you can simulate a cookie storage between successive Hurl runs.
.IP "--connect-timeout <seconds> "
Maximum time in seconds that you allow hurl's connection to take.
See also \fI-m, --max-time\fP option.
.IP "-c, --cookie-jar <filename> "
@ -202,6 +208,12 @@ This option explicitly allows Hurl to perform "insecure" SSL connections and tra
Follow redirect. You can limit the amount of redirects to follow by using the \fI--max-redirs\fP option.
.IP "-m, --max-time <seconds> "
Maximum time in seconds that you allow a request/response to take. This is the standard timeout.
See also \fI--connect-timeout\fP option.
.IP "--max-redirs <num> "

View File

@ -130,6 +130,12 @@ Read cookies from file (using the Netscape cookie file format).
Combined with [-c, --cookie-jar](#cookie-jar), you can simulate a cookie storage between successive Hurl runs.
### --connect-timeout <seconds> {#connect-timeout}
Maximum time in seconds that you allow hurl's connection to take.
See also [-m, --max-time](#max-time) option.
### -c, --cookie-jar <filename> {#cookie-jar}
@ -199,6 +205,12 @@ This option explicitly allows Hurl to perform "insecure" SSL connections and tra
Follow redirect. You can limit the amount of redirects to follow by using the [--max-redirs](#max-redirs) option.
### -m, --max-time <seconds> {#ax-time}
Maximum time in seconds that you allow a request/response to take. This is the standard timeout.
See also [--connect-timeout](#connect-timeout) option.
### --max-redirs <num> {#max-redirs}

View File

@ -1,2 +1,2 @@
<!DOCTYPE html>
<html><head><title>Hurl Report</title><link rel="stylesheet" type="text/css" href="report.css"></head><body><h2>Hurl Report</h2><div class="date">Wed, 23 Sep 2020 20:34:45 +0200</div><table><thead><tr><td>filename</td><td>duration</td></tr></thead><tbody><tr><td class="success">tests/assert_base64.hurl</td><td>0.002s</td></tr><tr><td class="success">tests/assert_header.hurl</td><td>0.002s</td></tr><tr><td class="success">tests/assert_json.hurl</td><td>0.008s</td></tr><tr><td class="success">tests/assert_match.hurl</td><td>0.016s</td></tr><tr><td class="success">tests/assert_regex.hurl</td><td>0.005s</td></tr><tr><td class="success">tests/assert_xpath.hurl</td><td>0.003s</td></tr><tr><td class="success">tests/bytes.hurl</td><td>0.002s</td></tr><tr><td class="success">tests/capture_and_assert.hurl</td><td>0.002s</td></tr><tr><td class="success">tests/captures.hurl</td><td>0.008s</td></tr><tr><td class="success">tests/cookies.hurl</td><td>0.016s</td></tr><tr><td class="success">tests/cookie_storage.hurl</td><td>0.003s</td></tr><tr><td class="success">tests/delete.hurl</td><td>0.001s</td></tr><tr><td class="success">tests/empty.hurl</td><td>0s</td></tr><tr><td class="success">tests/encoding.hurl</td><td>0.003s</td></tr><tr><td class="failure">tests/error_assert_base64.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_assert_file.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_assert_header_not_found.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_assert_header_value.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_assert_http_version.hurl</td><td>0.001s</td></tr><tr><td class="failure">tests/error_assert_invalid_predicate_type.hurl</td><td>0.003s</td></tr><tr><td class="failure">tests/error_assert_match_utf8.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_assert_query_cookie.hurl</td><td>0.004s</td></tr><tr><td class="failure">tests/error_assert_query_invalid_regex.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_assert_query_invalid_xpath.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_assert_status.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_assert_template_variable_not_found.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_assert_value_error.hurl</td><td>0.004s</td></tr><tr><td class="failure">tests/error_assert_variable.hurl</td><td>0.005s</td></tr><tr><td class="failure">tests/error_file_read_access.hurl</td><td>0s</td></tr><tr><td class="failure">tests/error_http_connection.hurl</td><td>0.01s</td></tr><tr><td class="failure">tests/error_invalid_jsonpath.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_invalid_url.hurl</td><td>0s</td></tr><tr><td class="failure">tests/error_invalid_xml.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_multipart_form_data.hurl</td><td>0s</td></tr><tr><td class="failure">tests/error_predicate.hurl</td><td>0.012s</td></tr><tr><td class="failure">tests/error_query_header_not_found.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_query_invalid_json.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_query_invalid_utf8.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_template_variable_not_found.hurl</td><td>0s</td></tr><tr><td class="failure">tests/error_template_variable_not_renderable.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/follow_redirect.hurl</td><td>0.002s</td></tr><tr><td class="success">tests/form_params.hurl</td><td>0.004s</td></tr><tr><td class="success">tests/headers.hurl</td><td>0.011s</td></tr><tr><td class="success">tests/hello.hurl</td><td>0.004s</td></tr><tr><td class="success">tests/multipart_form_data.hurl</td><td>0.003s</td></tr><tr><td class="success">tests/no_entry.hurl</td><td>0s</td></tr><tr><td class="success">tests/output.hurl</td><td>0.003s</td></tr><tr><td class="success">tests/patch.hurl</td><td>0.002s</td></tr><tr><td class="success">tests/post_base64.hurl</td><td>0.002s</td></tr><tr><td class="success">tests/post_file.hurl</td><td>0.002s</td></tr><tr><td class="success">tests/post_json.hurl</td><td>0.015s</td></tr><tr><td class="success">tests/post_multilines.hurl</td><td>0.005s</td></tr><tr><td class="success">tests/post_xml.hurl</td><td>0.004s</td></tr><tr><td class="success">tests/predicates-string.hurl</td><td>0.005s</td></tr><tr><td class="success">tests/put.hurl</td><td>0.001s</td></tr><tr><td class="success">tests/querystring_params.hurl</td><td>0.007s</td></tr><tr><td class="success">tests/redirect.hurl</td><td>0.003s</td></tr><tr><td class="success">tests/utf8.hurl</td><td>0.002s</td></tr></tbody></table></body></html>
<html><head><title>Hurl Report</title><link rel="stylesheet" type="text/css" href="report.css"></head><body><h2>Hurl Report</h2><div class="date">Thu, 24 Sep 2020 21:02:09 +0200</div><table><thead><tr><td>filename</td><td>duration</td></tr></thead><tbody><tr><td class="success">tests/assert_base64.hurl</td><td>0.002s</td></tr><tr><td class="success">tests/assert_header.hurl</td><td>0.003s</td></tr><tr><td class="success">tests/assert_json.hurl</td><td>0.009s</td></tr><tr><td class="success">tests/assert_match.hurl</td><td>0.016s</td></tr><tr><td class="success">tests/assert_regex.hurl</td><td>0.004s</td></tr><tr><td class="success">tests/assert_xpath.hurl</td><td>0.002s</td></tr><tr><td class="success">tests/bytes.hurl</td><td>0.002s</td></tr><tr><td class="success">tests/capture_and_assert.hurl</td><td>0.002s</td></tr><tr><td class="success">tests/captures.hurl</td><td>0.008s</td></tr><tr><td class="success">tests/cookies.hurl</td><td>0.016s</td></tr><tr><td class="success">tests/cookie_storage.hurl</td><td>0.003s</td></tr><tr><td class="success">tests/delete.hurl</td><td>0.002s</td></tr><tr><td class="success">tests/empty.hurl</td><td>0s</td></tr><tr><td class="success">tests/encoding.hurl</td><td>0.006s</td></tr><tr><td class="failure">tests/error_assert_base64.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_assert_file.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_assert_header_not_found.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_assert_header_value.hurl</td><td>0.001s</td></tr><tr><td class="failure">tests/error_assert_http_version.hurl</td><td>0.001s</td></tr><tr><td class="failure">tests/error_assert_invalid_predicate_type.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_assert_match_utf8.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_assert_query_cookie.hurl</td><td>0.004s</td></tr><tr><td class="failure">tests/error_assert_query_invalid_regex.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_assert_query_invalid_xpath.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_assert_status.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_assert_template_variable_not_found.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_assert_value_error.hurl</td><td>0.004s</td></tr><tr><td class="failure">tests/error_assert_variable.hurl</td><td>0.005s</td></tr><tr><td class="failure">tests/error_file_read_access.hurl</td><td>0s</td></tr><tr><td class="failure">tests/error_http_connection.hurl</td><td>0.006s</td></tr><tr><td class="failure">tests/error_invalid_jsonpath.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_invalid_url.hurl</td><td>0s</td></tr><tr><td class="failure">tests/error_invalid_xml.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_multipart_form_data.hurl</td><td>0s</td></tr><tr><td class="failure">tests/error_predicate.hurl</td><td>0.013s</td></tr><tr><td class="failure">tests/error_query_header_not_found.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_query_invalid_json.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_query_invalid_utf8.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_template_variable_not_found.hurl</td><td>0s</td></tr><tr><td class="failure">tests/error_template_variable_not_renderable.hurl</td><td>0.002s</td></tr><tr><td class="success">tests/error_timeout.hurl</td><td>2.004s</td></tr><tr><td class="failure">tests/follow_redirect.hurl</td><td>0.002s</td></tr><tr><td class="success">tests/form_params.hurl</td><td>0.004s</td></tr><tr><td class="success">tests/headers.hurl</td><td>0.011s</td></tr><tr><td class="success">tests/hello.hurl</td><td>0.003s</td></tr><tr><td class="success">tests/multipart_form_data.hurl</td><td>0.003s</td></tr><tr><td class="success">tests/no_entry.hurl</td><td>0s</td></tr><tr><td class="success">tests/output.hurl</td><td>0.003s</td></tr><tr><td class="success">tests/patch.hurl</td><td>0.002s</td></tr><tr><td class="success">tests/post_base64.hurl</td><td>0.002s</td></tr><tr><td class="success">tests/post_file.hurl</td><td>0.002s</td></tr><tr><td class="success">tests/post_json.hurl</td><td>0.015s</td></tr><tr><td class="success">tests/post_multilines.hurl</td><td>0.005s</td></tr><tr><td class="success">tests/post_xml.hurl</td><td>0.004s</td></tr><tr><td class="success">tests/predicates-string.hurl</td><td>0.005s</td></tr><tr><td class="success">tests/put.hurl</td><td>0.001s</td></tr><tr><td class="success">tests/querystring_params.hurl</td><td>0.007s</td></tr><tr><td class="success">tests/redirect.hurl</td><td>0.004s</td></tr><tr><td class="success">tests/utf8.hurl</td><td>0.002s</td></tr></tbody></table></body></html>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
error: Http Connection
--> tests/error_timeout.hurl:1:5
|
1 | GET http://localhost:8000/timeout
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Timeout has been reached
|

View File

@ -0,0 +1 @@
3

View File

@ -0,0 +1,3 @@
GET http://localhost:8000/timeout

View File

@ -0,0 +1 @@
--max-time 1

View File

@ -0,0 +1,9 @@
from tests import app
import time
@app.route('/timeout')
def timeout():
time.sleep(2)
return ''

View File

@ -36,6 +36,7 @@ use hurl::parser;
use hurl::runner;
use hurl::runner::core::*;
use hurl::runner::log_deserialize;
use std::time::Duration;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CLIOptions {
@ -50,6 +51,8 @@ pub struct CLIOptions {
pub proxy: Option<String>,
pub no_proxy: Option<String>,
pub cookie_input_file: Option<String>,
pub timeout: Duration,
pub connect_timeout: Duration,
}
fn execute(
@ -114,6 +117,9 @@ fn execute(
let proxy = cli_options.proxy;
let no_proxy = cli_options.no_proxy;
let cookie_input_file = cli_options.cookie_input_file;
let timeout = cli_options.timeout;
let connect_timeout = cli_options.connect_timeout;
let options = http::ClientOptions {
follow_location,
max_redirect,
@ -122,6 +128,8 @@ fn execute(
no_proxy,
verbose,
insecure,
timeout,
connect_timeout,
};
let mut client = http::Client::init(options);
@ -298,6 +306,12 @@ fn app() -> clap::App<'static, 'static> {
.conflicts_with("no-color")
.help("Colorize Output"),
)
.arg(
clap::Arg::with_name("connect_timeout")
.long("connect-timeout")
.value_name("SECONDS")
.help("Maximum time allowed for connection"),
)
.arg(
clap::Arg::with_name("cookies_input_file")
.short("b")
@ -357,6 +371,14 @@ fn app() -> clap::App<'static, 'static> {
.long("location")
.help("Follow redirects"),
)
.arg(
clap::Arg::with_name("max_time")
.long("max-time")
.short("m")
.value_name("NUM")
.allow_hyphen_values(true)
.help("Maximum time allowed for the transfer"),
)
.arg(
clap::Arg::with_name("max_redirects")
.long("max-redirs")
@ -447,7 +469,31 @@ fn parse_options(
Err(_) => {
return Err(cli::Error {
message: "max_redirs option can not be parsed".to_string(),
})
});
}
},
};
let timeout = match matches.value_of("max_time") {
None => Duration::from_secs(0),
Some(s) => match s.parse::<u64>() {
Ok(n) => Duration::from_secs(n),
Err(_) => {
return Err(cli::Error {
message: "max_time option can not be parsed".to_string(),
});
}
},
};
let connect_timeout = match matches.value_of("connect_timeout") {
None => Duration::from_secs(300),
Some(s) => match s.parse::<u64>() {
Ok(n) => Duration::from_secs(n),
Err(_) => {
return Err(cli::Error {
message: "connect-timeout option can not be parsed".to_string(),
});
}
},
};
@ -464,6 +510,8 @@ fn parse_options(
proxy,
no_proxy,
cookie_input_file,
timeout,
connect_timeout,
})
}

View File

@ -22,6 +22,7 @@ use std::str;
use curl::easy;
use encoding::all::ISO_8859_1;
use encoding::{DecoderTrap, Encoding};
use std::time::Duration;
use super::core::*;
use super::request::*;
@ -36,6 +37,7 @@ pub enum HttpError {
CouldNotParseResponse,
SSLCertificate,
InvalidUrl,
Timeout,
Other { description: String, code: u32 },
}
@ -61,6 +63,8 @@ pub struct ClientOptions {
pub no_proxy: Option<String>,
pub verbose: bool,
pub insecure: bool,
pub timeout: Duration,
pub connect_timeout: Duration,
}
impl Client {
@ -90,6 +94,9 @@ impl Client {
h.ssl_verify_host(options.insecure).unwrap();
h.ssl_verify_peer(options.insecure).unwrap();
h.timeout(options.timeout).unwrap();
h.connect_timeout(options.connect_timeout).unwrap();
Client {
handle: Box::new(h),
follow_location: options.follow_location,
@ -177,6 +184,7 @@ impl Client {
5 => Err(HttpError::CouldNotResolveProxyName),
6 => Err(HttpError::CouldNotResolveHost),
7 => Err(HttpError::FailToConnect),
28 => Err(HttpError::Timeout),
60 => Err(HttpError::SSLCertificate),
_ => Err(HttpError::Other {
code: e.code(),

View File

@ -122,6 +122,7 @@ pub enum RunnerError {
CouldNotResolveProxyName,
CouldNotResolveHost,
FailToConnect,
Timeout,
TooManyRedirect,
CouldNotParseResponse,
SSLCertificate,
@ -192,6 +193,7 @@ impl FormatError for Error {
RunnerError::CouldNotResolveProxyName => "Http Connection".to_string(),
RunnerError::CouldNotResolveHost => "Http Connection".to_string(),
RunnerError::FailToConnect => "Http Connection".to_string(),
RunnerError::Timeout => "Http Connection".to_string(),
RunnerError::TooManyRedirect => "Http Connection".to_string(),
RunnerError::CouldNotParseResponse => "Http Connection".to_string(),
RunnerError::SSLCertificate => "Http Connection".to_string(),
@ -230,6 +232,7 @@ impl FormatError for Error {
RunnerError::CouldNotResolveProxyName => "Could not resolve proxy name".to_string(),
RunnerError::CouldNotResolveHost => "Could not resolve host".to_string(),
RunnerError::FailToConnect => "Fail to connect".to_string(),
RunnerError::Timeout => "Timeout has been reached".to_string(),
RunnerError::TooManyRedirect => "Too many redirect".to_string(),
RunnerError::CouldNotParseResponse => "Could not parse response".to_string(),
RunnerError::SSLCertificate => "SSl Certificate".to_string(),

View File

@ -105,6 +105,7 @@ pub fn run(
HttpError::CouldNotResolveProxyName => RunnerError::CouldNotResolveProxyName,
HttpError::CouldNotResolveHost => RunnerError::CouldNotResolveHost,
HttpError::FailToConnect => RunnerError::FailToConnect,
HttpError::Timeout => RunnerError::Timeout,
HttpError::TooManyRedirect => RunnerError::TooManyRedirect,
HttpError::CouldNotParseResponse => RunnerError::CouldNotParseResponse,
HttpError::SSLCertificate => RunnerError::SSLCertificate,

View File

@ -53,6 +53,8 @@ use crate::core::common::FormatError;
/// no_proxy: None,
/// verbose: false,
/// insecure: false,
/// timeout: Default::default(),
/// connect_timeout: Default::default()
/// };
/// let mut client = http::Client::init(options);
///

View File

@ -1,7 +1,7 @@
use curl::easy::Easy;
use std::fs::File;
use std::io::prelude::*;
use curl::easy::Easy;
use std::time::Duration;
use hurl::http::*;
@ -63,6 +63,8 @@ fn default_client() -> Client {
no_proxy: None,
verbose: true,
insecure: false,
timeout: Default::default(),
connect_timeout: Duration::from_secs(300),
};
Client::init(options)
}
@ -302,6 +304,8 @@ fn test_follow_location() {
no_proxy: None,
verbose: false,
insecure: false,
timeout: Default::default(),
connect_timeout: Default::default(),
};
let mut client = Client::init(options);
let response = client.execute(&request, 0).unwrap();
@ -333,6 +337,8 @@ fn test_max_redirect() {
no_proxy: None,
verbose: false,
insecure: false,
timeout: Default::default(),
connect_timeout: Default::default(),
};
let mut client = Client::init(options);
let request = default_get_request("http://localhost:8000/redirect".to_string());
@ -447,6 +453,8 @@ fn test_error_fail_to_connect() {
no_proxy: None,
verbose: true,
insecure: false,
timeout: Default::default(),
connect_timeout: Default::default(),
};
let mut client = Client::init(options);
let request = default_get_request("http://localhost:8000/hello".to_string());
@ -464,6 +472,8 @@ fn test_error_could_not_resolve_proxy_name() {
no_proxy: None,
verbose: false,
insecure: false,
timeout: Default::default(),
connect_timeout: Default::default(),
};
let mut client = Client::init(options);
let request = default_get_request("http://localhost:8000/hello".to_string());
@ -471,6 +481,46 @@ fn test_error_could_not_resolve_proxy_name() {
assert_eq!(error, HttpError::CouldNotResolveProxyName);
}
#[test]
fn test_timeout() {
let options = ClientOptions {
follow_location: false,
max_redirect: None,
cookie_input_file: None,
proxy: None,
no_proxy: None,
verbose: false,
insecure: false,
timeout: Duration::from_millis(100),
connect_timeout: Default::default(),
};
let mut client = Client::init(options);
let request = default_get_request("http://localhost:8000/timeout".to_string());
let error = client.execute(&request, 0).err().unwrap();
assert_eq!(error, HttpError::Timeout);
}
//
// TODO: find a way to test it locally
// #[test]
fn test_connect_timeout() {
let options = ClientOptions {
follow_location: false,
max_redirect: None,
cookie_input_file: None,
proxy: None,
no_proxy: None,
verbose: false,
insecure: false,
timeout: Default::default(),
connect_timeout: Duration::from_secs(1),
};
let mut client = Client::init(options);
let request = default_get_request("http://example.com:81".to_string());
let error = client.execute(&request, 0).err().unwrap();
assert_eq!(error, HttpError::Timeout);
}
// endregion
// region cookie
@ -564,6 +614,8 @@ fn test_cookie_file() {
no_proxy: None,
verbose: false,
insecure: false,
timeout: Default::default(),
connect_timeout: Default::default(),
};
let mut client = Client::init(options);
let request = default_get_request(
@ -589,6 +641,8 @@ fn test_proxy() {
no_proxy: None,
verbose: false,
insecure: false,
timeout: Default::default(),
connect_timeout: Default::default(),
};
let mut client = Client::init(options);
let request = default_get_request("http://localhost:8000/hello".to_string());

View File

@ -44,6 +44,8 @@ fn test_hurl_file() {
no_proxy: None,
verbose: false,
insecure: false,
timeout: Default::default(),
connect_timeout: Default::default(),
};
let mut client = http::Client::init(options);
let mut lines: Vec<&str> = regex::Regex::new(r"\n|\r\n")
@ -149,6 +151,8 @@ fn test_hello() {
no_proxy: None,
verbose: false,
insecure: false,
timeout: Default::default(),
connect_timeout: Default::default(),
};
let mut client = http::Client::init(options);
let source_info = SourceInfo {