diff --git a/rs/https_outcalls/adapter/src/rpc_server.rs b/rs/https_outcalls/adapter/src/rpc_server.rs index 0d4ff9391237..ce20c2abe66d 100644 --- a/rs/https_outcalls/adapter/src/rpc_server.rs +++ b/rs/https_outcalls/adapter/src/rpc_server.rs @@ -137,11 +137,17 @@ impl CanisterHttp { http_connector .set_connect_timeout(Some(Duration::from_secs(self.http_connect_timeout_secs))); + let builder = HttpsConnectorBuilder::new() + .with_native_roots() + .expect("Failed to set native roots"); + + #[cfg(not(feature = "http"))] + let builder = builder.https_only(); + #[cfg(feature = "http")] + let builder = builder.https_or_http(); + Client::builder(TokioExecutor::new()).build::<_, Full>( - HttpsConnectorBuilder::new() - .with_native_roots() - .expect("Failed to set native roots") - .https_only() + builder .enable_all_versions() .wrap_connector(SocksConnector { proxy_addr, diff --git a/rs/rust_canisters/proxy_canister/BUILD.bazel b/rs/rust_canisters/proxy_canister/BUILD.bazel index a8fa609f00f9..6746ad487196 100644 --- a/rs/rust_canisters/proxy_canister/BUILD.bazel +++ b/rs/rust_canisters/proxy_canister/BUILD.bazel @@ -5,6 +5,7 @@ package(default_visibility = ["//visibility:public"]) rust_library( name = "lib", + testonly = True, srcs = ["src/lib.rs"], crate_name = "proxy_canister", version = "0.1.0", @@ -12,12 +13,14 @@ rust_library( # Keep sorted. "//rs/types/management_canister_types", "@crate_index//:candid", + "@crate_index//:ic_cdk_0_17_1", "@crate_index//:serde", ], ) rust_canister( name = "proxy_canister", + testonly = True, srcs = ["src/main.rs"], proc_macro_deps = ["@crate_index//:ic_cdk_macros_0_17_1"], service_file = ":empty.did", diff --git a/rs/rust_canisters/proxy_canister/src/lib.rs b/rs/rust_canisters/proxy_canister/src/lib.rs index 7f933ad7ca01..1a41d1a943a6 100644 --- a/rs/rust_canisters/proxy_canister/src/lib.rs +++ b/rs/rust_canisters/proxy_canister/src/lib.rs @@ -8,6 +8,7 @@ use std::time::Duration; use candid::{CandidType, Deserialize}; +use ic_cdk::api::call::RejectionCode; use ic_management_canister_types_private::{ BoundedHttpHeaders, HttpHeader, HttpMethod, Payload, TransformContext, }; @@ -61,6 +62,12 @@ pub struct RemoteHttpResponse { pub body: String, } +#[derive(Clone, Debug, CandidType, Deserialize)] +pub struct ResponseWithRefundedCycles { + pub result: Result, + pub refunded_cycles: u64, +} + #[derive(Clone, Debug, CandidType, Deserialize)] pub struct RemoteHttpStressResponse { pub response: RemoteHttpResponse, diff --git a/rs/rust_canisters/proxy_canister/src/main.rs b/rs/rust_canisters/proxy_canister/src/main.rs index e14fbc16b06b..ade2aa5a8d70 100644 --- a/rs/rust_canisters/proxy_canister/src/main.rs +++ b/rs/rust_canisters/proxy_canister/src/main.rs @@ -16,6 +16,7 @@ use ic_management_canister_types_private::{ }; use proxy_canister::{ RemoteHttpRequest, RemoteHttpResponse, RemoteHttpStressRequest, RemoteHttpStressResponse, + ResponseWithRefundedCycles, }; use std::cell::RefCell; use std::collections::HashMap; @@ -117,14 +118,13 @@ async fn run_continuous_request_loop(request: RemoteHttpRequest) { }); } -#[update] -async fn send_request( +async fn make_http_request_with_refund_callback( request: RemoteHttpRequest, -) -> Result { +) -> (Result, u64) { let RemoteHttpRequest { request, cycles } = request; let request_url = request.url.clone(); println!("send_request making IC call."); - match ic_cdk::api::call::call_raw( + let result = match ic_cdk::api::call::call_raw( Principal::management_canister(), "http_request", &request.encode(), @@ -163,7 +163,25 @@ async fn send_request( }); Err((r, m)) } - } + }; + let refund = ic_cdk::api::call::msg_cycles_refunded(); + (result, refund) +} + +#[update] +async fn send_request( + request: RemoteHttpRequest, +) -> Result { + let (result, _) = make_http_request_with_refund_callback(request).await; + result +} + +#[update] +async fn send_request_and_retrieve_refund( + request: RemoteHttpRequest, +) -> ResponseWithRefundedCycles { + let (result, refunded_cycles) = make_http_request_with_refund_callback(request).await; + ResponseWithRefundedCycles{result, refunded_cycles} } #[query] diff --git a/rs/tests/networking/canister_http_correctness_test.rs b/rs/tests/networking/canister_http_correctness_test.rs index 73a29e4294ff..e8edd52cd294 100644 --- a/rs/tests/networking/canister_http_correctness_test.rs +++ b/rs/tests/networking/canister_http_correctness_test.rs @@ -45,7 +45,10 @@ use ic_types::{ canister_http::{CanisterHttpRequestContext, MAX_CANISTER_HTTP_REQUEST_BYTES}, time::UNIX_EPOCH, }; -use proxy_canister::{RemoteHttpRequest, RemoteHttpResponse, UnvalidatedCanisterHttpRequestArgs}; +use proxy_canister::{ + RemoteHttpRequest, RemoteHttpResponse, ResponseWithRefundedCycles, + UnvalidatedCanisterHttpRequestArgs, +}; use serde_json::Value; use std::{collections::HashSet, convert::TryFrom}; @@ -58,6 +61,7 @@ const MAX_HEADER_VALUE_LENGTH: usize = 8 * 1024; const TOTAL_HEADER_NAME_AND_VALUE_LENGTH: usize = 48 * 1024; const HTTP_HEADERS_MAX_NUMBER: usize = 64; const RESPONSE_OVERHEAD: u64 = 256; +const HTTP_REQUEST_CYCLE_PAYMENT: u64 = 500_000_000_000; // httpbin-rs returns 5 headers in addition to the requested headers: // content-type, access-control-allow-origin, access-control-allow-credentials, date, content-length. @@ -194,7 +198,7 @@ fn test_enforce_https(env: TestEnv) { let handlers = Handlers::new(&env); let webserver_ipv6 = get_universal_vm_address(&env); - let response = block_on(submit_outcall( + let (response, _) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: UnvalidatedCanisterHttpRequestArgs { @@ -211,7 +215,7 @@ fn test_enforce_https(env: TestEnv) { }), max_response_bytes: None, }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -230,7 +234,7 @@ fn test_transform_function_is_executed(env: TestEnv) { let transform_context = "transform_context".as_bytes().to_vec(); - let response = block_on(submit_outcall( + let (response, _) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: UnvalidatedCanisterHttpRequestArgs { @@ -247,7 +251,7 @@ fn test_transform_function_is_executed(env: TestEnv) { }), max_response_bytes: None, }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -272,7 +276,7 @@ fn test_non_existent_transform_function(env: TestEnv) { let transform_context = "transform_context".as_bytes().to_vec(); - let response = block_on(submit_outcall( + let (response, refunded_cycles) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: UnvalidatedCanisterHttpRequestArgs { @@ -289,7 +293,7 @@ fn test_non_existent_transform_function(env: TestEnv) { }), max_response_bytes: None, }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -300,13 +304,16 @@ fn test_non_existent_transform_function(env: TestEnv) { .. }) ); + assert_ne!( + refunded_cycles, RefundedCycles::Cycles(HTTP_REQUEST_CYCLE_PAYMENT) + ); } fn test_composite_transform_function_is_executed(env: TestEnv) { let handlers = Handlers::new(&env); let webserver_ipv6 = get_universal_vm_address(&env); - let response = block_on(submit_outcall( + let (response, _) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: UnvalidatedCanisterHttpRequestArgs { @@ -323,7 +330,7 @@ fn test_composite_transform_function_is_executed(env: TestEnv) { }), max_response_bytes: None, }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -340,7 +347,7 @@ fn test_no_cycles_attached(env: TestEnv) { let handlers = Handlers::new(&env); let webserver_ipv6 = get_universal_vm_address(&env); - let response = block_on(submit_outcall( + let (response, _) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: UnvalidatedCanisterHttpRequestArgs { @@ -390,7 +397,7 @@ fn test_max_possible_request_size(env: TestEnv) { let body = vec![0; MAX_REQUEST_BYTES_LIMIT - header_list_size]; - let response = block_on(submit_outcall( + let (response, _) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: UnvalidatedCanisterHttpRequestArgs { @@ -407,7 +414,7 @@ fn test_max_possible_request_size(env: TestEnv) { }), max_response_bytes: None, }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -434,7 +441,7 @@ fn test_max_possible_request_size_exceeded(env: TestEnv) { let body = vec![0; MAX_REQUEST_BYTES_LIMIT - header_list_size + 1]; - let response = block_on(submit_outcall( + let (response, refunded_cycles) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: UnvalidatedCanisterHttpRequestArgs { @@ -451,7 +458,7 @@ fn test_max_possible_request_size_exceeded(env: TestEnv) { }), max_response_bytes: None, }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -462,6 +469,9 @@ fn test_max_possible_request_size_exceeded(env: TestEnv) { .. }) ); + assert_eq!( + refunded_cycles, RefundedCycles::Cycles(HTTP_REQUEST_CYCLE_PAYMENT) + ); } fn test_2mb_response_cycle_for_rejection_path(env: TestEnv) { @@ -483,7 +493,7 @@ fn test_2mb_response_cycle_for_rejection_path(env: TestEnv) { max_response_bytes: None, }; - let response = block_on(async move { + let (response, _) = block_on(async move { submit_outcall( &handlers, RemoteHttpRequest { @@ -526,7 +536,7 @@ fn test_4096_max_response_cycle_case_1(env: TestEnv) { max_response_bytes: Some(16384), }; - let response = block_on(async move { + let (response, _) = block_on(async move { submit_outcall( &handlers, RemoteHttpRequest { @@ -563,7 +573,7 @@ fn test_4096_max_response_cycle_case_2(env: TestEnv) { max_response_bytes: Some(16384), }; - let response = block_on(async move { + let (response, _) = block_on(async move { submit_outcall( &handlers, RemoteHttpRequest { @@ -590,7 +600,7 @@ fn test_max_response_bytes_2_mb_returns_ok(env: TestEnv) { let handlers = Handlers::new(&env); let webserver_ipv6 = get_universal_vm_address(&env); - let response = block_on(submit_outcall( + let (response, _) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: UnvalidatedCanisterHttpRequestArgs { @@ -607,7 +617,7 @@ fn test_max_response_bytes_2_mb_returns_ok(env: TestEnv) { }), max_response_bytes: Some((MAX_MAX_RESPONSE_BYTES) as u64), }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -618,7 +628,7 @@ fn test_max_response_bytes_too_large(env: TestEnv) { let handlers = Handlers::new(&env); let webserver_ipv6 = get_universal_vm_address(&env); - let response = block_on(submit_outcall( + let (response, refunded_cycles) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: UnvalidatedCanisterHttpRequestArgs { @@ -635,7 +645,7 @@ fn test_max_response_bytes_too_large(env: TestEnv) { }), max_response_bytes: Some((MAX_MAX_RESPONSE_BYTES + 1) as u64), }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -646,13 +656,16 @@ fn test_max_response_bytes_too_large(env: TestEnv) { .. }) ); + assert_eq!( + refunded_cycles, RefundedCycles::Cycles(HTTP_REQUEST_CYCLE_PAYMENT) + ); } fn test_transform_that_bloats_on_the_2mb_limit(env: TestEnv) { let handlers = Handlers::new(&env); let webserver_ipv6 = get_universal_vm_address(&env); - let response = block_on(submit_outcall( + let (response, _) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: UnvalidatedCanisterHttpRequestArgs { @@ -669,7 +682,7 @@ fn test_transform_that_bloats_on_the_2mb_limit(env: TestEnv) { }), max_response_bytes: None, }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -680,7 +693,7 @@ fn test_transform_that_bloats_response_above_2mb_limit(env: TestEnv) { let handlers = Handlers::new(&env); let webserver_ipv6 = get_universal_vm_address(&env); - let response = block_on(submit_outcall( + let (response, refunded_cycles) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: UnvalidatedCanisterHttpRequestArgs { @@ -697,7 +710,7 @@ fn test_transform_that_bloats_response_above_2mb_limit(env: TestEnv) { }), max_response_bytes: None, }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -708,13 +721,16 @@ fn test_transform_that_bloats_response_above_2mb_limit(env: TestEnv) { .. }) ); + assert_ne!( + refunded_cycles, RefundedCycles::Cycles(HTTP_REQUEST_CYCLE_PAYMENT) + ); } fn test_post_request(env: TestEnv) { let handlers = Handlers::new(&env); let webserver_ipv6 = get_universal_vm_address(&env); - let response = block_on(submit_outcall( + let (response, _) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: UnvalidatedCanisterHttpRequestArgs { @@ -734,7 +750,7 @@ fn test_post_request(env: TestEnv) { }), max_response_bytes: None, }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -746,7 +762,7 @@ fn test_http_endpoint_response_is_within_limits_with_custom_max_response_bytes(e let webserver_ipv6 = get_universal_vm_address(&env); let max_response_bytes: u64 = 1_000_000; - let response = block_on(submit_outcall( + let (response, _) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: UnvalidatedCanisterHttpRequestArgs { @@ -768,10 +784,10 @@ fn test_http_endpoint_response_is_within_limits_with_custom_max_response_bytes(e // a 256B leeway is decent.. max_response_bytes: Some(max_response_bytes + RESPONSE_OVERHEAD), }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, - )) - .expect("Request is successful."); + )); + let response = response.expect("Request is successful."); assert_matches!(&response, RemoteHttpResponse { status: 200, .. }); } @@ -781,7 +797,7 @@ fn test_http_endpoint_response_is_too_large_with_custom_max_response_bytes(env: let webserver_ipv6 = get_universal_vm_address(&env); let max_response_bytes = 1_000_000; - let response = block_on(submit_outcall( + let (response, _) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: UnvalidatedCanisterHttpRequestArgs { @@ -801,7 +817,7 @@ fn test_http_endpoint_response_is_too_large_with_custom_max_response_bytes(env: }), max_response_bytes: Some(max_response_bytes), }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -818,7 +834,7 @@ fn test_http_endpoint_response_is_within_limits_with_default_max_response_bytes( let handlers = Handlers::new(&env); let webserver_ipv6 = get_universal_vm_address(&env); - let response = block_on(submit_outcall( + let (response, _) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: UnvalidatedCanisterHttpRequestArgs { @@ -840,10 +856,10 @@ fn test_http_endpoint_response_is_within_limits_with_default_max_response_bytes( }), max_response_bytes: None, }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, - )) - .expect("Request is successful."); + )); + let response = response.expect("Request is successful."); assert_matches!(&response, RemoteHttpResponse { status: 200, .. }); } @@ -852,7 +868,7 @@ fn test_http_endpoint_response_is_too_large_with_default_max_response_bytes(env: let handlers = Handlers::new(&env); let webserver_ipv6 = get_universal_vm_address(&env); - let response = block_on(submit_outcall( + let (response, _) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: UnvalidatedCanisterHttpRequestArgs { @@ -872,7 +888,7 @@ fn test_http_endpoint_response_is_too_large_with_default_max_response_bytes(env: }), max_response_bytes: None, }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -889,7 +905,7 @@ fn test_http_endpoint_with_delayed_response_is_rejected(env: TestEnv) { let handlers = Handlers::new(&env); let webserver_ipv6 = get_universal_vm_address(&env); - let response = block_on(submit_outcall( + let (response, _) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: UnvalidatedCanisterHttpRequestArgs { @@ -906,7 +922,7 @@ fn test_http_endpoint_with_delayed_response_is_rejected(env: TestEnv) { }), max_response_bytes: None, }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -924,7 +940,7 @@ fn test_that_redirects_are_not_followed(env: TestEnv) { let handlers = Handlers::new(&env); let webserver_ipv6 = get_universal_vm_address(&env); - let response = block_on(submit_outcall( + let (response, _) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: UnvalidatedCanisterHttpRequestArgs { @@ -941,7 +957,7 @@ fn test_that_redirects_are_not_followed(env: TestEnv) { }), max_response_bytes: None, }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -953,7 +969,7 @@ fn test_http_calls_to_ic_fails(env: TestEnv) { let handlers = Handlers::new(&env); let webserver_ipv6 = get_universal_vm_address(&env); - let response = block_on(submit_outcall( + let (response, _) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: UnvalidatedCanisterHttpRequestArgs { @@ -970,7 +986,7 @@ fn test_http_calls_to_ic_fails(env: TestEnv) { }), max_response_bytes: None, }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -987,11 +1003,10 @@ fn test_http_calls_to_ic_fails(env: TestEnv) { ); } -// ---- BEGIN SPEC COMPLIANCE TESTS ---- fn test_invalid_domain_name(env: TestEnv) { let handlers = Handlers::new(&env); - let response = block_on(submit_outcall( + let (response, refunded_cycles) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: UnvalidatedCanisterHttpRequestArgs { @@ -1008,7 +1023,7 @@ fn test_invalid_domain_name(env: TestEnv) { }), max_response_bytes: None, }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -1019,12 +1034,15 @@ fn test_invalid_domain_name(env: TestEnv) { .. }) ); + assert_ne!( + refunded_cycles, RefundedCycles::Cycles(HTTP_REQUEST_CYCLE_PAYMENT) + ); } fn test_invalid_ip(env: TestEnv) { let handlers = Handlers::new(&env); - let response = block_on(submit_outcall( + let (response, refunded_cycles) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: UnvalidatedCanisterHttpRequestArgs { @@ -1041,7 +1059,7 @@ fn test_invalid_ip(env: TestEnv) { }), max_response_bytes: None, }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -1052,6 +1070,9 @@ fn test_invalid_ip(env: TestEnv) { .. }) ); + assert_ne!( + refunded_cycles, RefundedCycles::Cycles(HTTP_REQUEST_CYCLE_PAYMENT) + ); } /// Test that the response body returned is the same as the requested path. @@ -1076,16 +1097,19 @@ fn test_get_hello_world_call(env: TestEnv) { max_response_bytes: Some(max_response_bytes), }; - let response = block_on(submit_outcall( + let (response, refunded_cycles) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: request.clone(), - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, - )) - .expect("Request is successful."); + )); + let response = response.expect("Request is successful."); assert_matches!(&response, RemoteHttpResponse {body, status: 200, ..} if body == expected_body); + assert_ne!( + refunded_cycles, RefundedCycles::Cycles(HTTP_REQUEST_CYCLE_PAYMENT) + ); assert_http_response(&response); } @@ -1114,16 +1138,19 @@ fn test_request_header_total_size_within_the_48_kib_limit(env: TestEnv) { max_response_bytes: None, }; - let response = block_on(submit_outcall( + let (response, refunded_cycles) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: request.clone(), - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, - )) - .expect("Request succeeds."); + )); + let response = response.expect("Request succeeds."); assert_matches!(&response, RemoteHttpResponse { status: 200, .. }); + assert_ne!( + refunded_cycles, RefundedCycles::Cycles(HTTP_REQUEST_CYCLE_PAYMENT) + ); } fn test_request_header_total_size_over_the_48_kib_limit(env: TestEnv) { @@ -1156,11 +1183,11 @@ fn test_request_header_total_size_over_the_48_kib_limit(env: TestEnv) { max_response_bytes: None, }; - let response = block_on(submit_outcall( + let (response, refunded_cycles) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: request.clone(), - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -1171,6 +1198,9 @@ fn test_request_header_total_size_over_the_48_kib_limit(env: TestEnv) { .. }) ); + assert_eq!( + refunded_cycles, RefundedCycles::Cycles(HTTP_REQUEST_CYCLE_PAYMENT) + ); } fn test_response_header_total_size_within_the_48_kib_limit(env: TestEnv) { @@ -1185,7 +1215,7 @@ fn test_response_header_total_size_within_the_48_kib_limit(env: TestEnv) { webserver_ipv6, MAX_HEADER_NAME_LENGTH, TOTAL_HEADER_NAME_AND_VALUE_LENGTH, ); - let response = block_on(submit_outcall( + let (response, refunded_cycles) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: UnvalidatedCanisterHttpRequestArgs { @@ -1196,11 +1226,14 @@ fn test_response_header_total_size_within_the_48_kib_limit(env: TestEnv) { transform: None, max_response_bytes: Some(DEFAULT_MAX_RESPONSE_BYTES), }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); assert_matches!(&response, Ok(RemoteHttpResponse { status: 200, .. })); + assert_ne!( + refunded_cycles, RefundedCycles::Cycles(HTTP_REQUEST_CYCLE_PAYMENT) + ); // Compute exactly the size of the response headers to account also for overhead. let total_header_size: usize = response @@ -1232,7 +1265,7 @@ fn test_response_header_total_size_over_the_48_kib_limit(env: TestEnv) { TOTAL_HEADER_NAME_AND_VALUE_LENGTH + 1, ); - let response = block_on(submit_outcall( + let (response, refunded_cycles) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: UnvalidatedCanisterHttpRequestArgs { @@ -1243,7 +1276,7 @@ fn test_response_header_total_size_over_the_48_kib_limit(env: TestEnv) { transform: None, max_response_bytes: Some(DEFAULT_MAX_RESPONSE_BYTES), }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -1254,6 +1287,9 @@ fn test_response_header_total_size_over_the_48_kib_limit(env: TestEnv) { .. }) ); + assert_ne!( + refunded_cycles, RefundedCycles::Cycles(HTTP_REQUEST_CYCLE_PAYMENT) + ); } fn test_request_header_name_and_value_within_limits(env: TestEnv) { @@ -1274,14 +1310,14 @@ fn test_request_header_name_and_value_within_limits(env: TestEnv) { max_response_bytes: None, }; - let response = block_on(submit_outcall( + let (response, _) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: request.clone(), - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, - )) - .expect("Request succeeds."); + )); + let response = response.expect("Request succeeds."); assert_matches!(&response, RemoteHttpResponse { status: 200, .. }); } @@ -1304,11 +1340,11 @@ fn test_request_header_name_too_long(env: TestEnv) { max_response_bytes: None, }; - let response = block_on(submit_outcall( + let (response, refunded_cycles) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: request.clone(), - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -1319,6 +1355,9 @@ fn test_request_header_name_too_long(env: TestEnv) { .. }) ); + assert_eq!( + refunded_cycles, RefundedCycles::Cycles(HTTP_REQUEST_CYCLE_PAYMENT) + ); } fn test_request_header_value_too_long(env: TestEnv) { @@ -1339,11 +1378,11 @@ fn test_request_header_value_too_long(env: TestEnv) { max_response_bytes: None, }; - let response = block_on(submit_outcall( + let (response, refunded_cycles) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: request.clone(), - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -1354,6 +1393,9 @@ fn test_request_header_value_too_long(env: TestEnv) { .. }) ); + assert_eq!( + refunded_cycles, RefundedCycles::Cycles(HTTP_REQUEST_CYCLE_PAYMENT) + ); } fn test_response_header_name_within_limit(env: TestEnv) { @@ -1365,7 +1407,7 @@ fn test_response_header_name_within_limit(env: TestEnv) { webserver_ipv6, MAX_HEADER_NAME_LENGTH, ); - let response = block_on(submit_outcall( + let (response, _) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: UnvalidatedCanisterHttpRequestArgs { @@ -1376,7 +1418,7 @@ fn test_response_header_name_within_limit(env: TestEnv) { transform: None, max_response_bytes: None, }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -1393,7 +1435,7 @@ fn test_response_header_name_over_limit(env: TestEnv) { MAX_HEADER_NAME_LENGTH + 1, ); - let response = block_on(submit_outcall( + let (response, refunded_cycles) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: UnvalidatedCanisterHttpRequestArgs { @@ -1404,7 +1446,7 @@ fn test_response_header_name_over_limit(env: TestEnv) { transform: None, max_response_bytes: None, }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -1415,6 +1457,10 @@ fn test_response_header_name_over_limit(env: TestEnv) { .. }) ); + + assert_ne!( + refunded_cycles, RefundedCycles::Cycles(HTTP_REQUEST_CYCLE_PAYMENT) + ); } fn test_response_header_value_within_limit(env: TestEnv) { @@ -1435,11 +1481,11 @@ fn test_response_header_value_within_limit(env: TestEnv) { max_response_bytes: None, }; - let response = block_on(submit_outcall( + let (response, _) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: request.clone(), - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -1465,11 +1511,11 @@ fn test_response_header_value_over_limit(env: TestEnv) { max_response_bytes: None, }; - let response = block_on(submit_outcall( + let (response, refunded_cycles) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: request.clone(), - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -1480,6 +1526,9 @@ fn test_response_header_value_over_limit(env: TestEnv) { .. }) ); + assert_ne!( + refunded_cycles, RefundedCycles::Cycles(HTTP_REQUEST_CYCLE_PAYMENT) + ); } fn test_post_call(env: TestEnv) { @@ -1510,14 +1559,14 @@ fn test_post_call(env: TestEnv) { max_response_bytes, }; - let response = block_on(submit_outcall( + let (response, _) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: request.clone(), - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, - )) - .expect("Request succeeds."); + )); + let response = response.expect("Request succeeds."); assert_matches!(&response, RemoteHttpResponse {body, status: 200, ..} if body.contains(expected_body)); assert_distinct_headers(&response); @@ -1558,14 +1607,14 @@ fn test_head_call(env: TestEnv) { max_response_bytes, }; - let response = block_on(submit_outcall( + let (response, _) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: request.clone(), - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, - )) - .expect("Request succeeds."); + )); + let response = response.expect("Request succeeds."); assert_matches!(&response, RemoteHttpResponse { status: 200, .. }); assert_distinct_headers(&response); @@ -1599,7 +1648,7 @@ fn test_only_headers_with_custom_max_response_bytes(env: TestEnv) { let header_size = 142; let max_response_bytes = Some(header_size + n); - let response = block_on(submit_outcall( + let (response, _) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: UnvalidatedCanisterHttpRequestArgs { @@ -1610,10 +1659,10 @@ fn test_only_headers_with_custom_max_response_bytes(env: TestEnv) { transform: None, max_response_bytes, }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, - )) - .expect("Request is successful."); + )); + let response = response.expect("Request is successful."); assert_matches!(&response, RemoteHttpResponse { status: 200, .. }); assert_http_response(&response); @@ -1637,7 +1686,7 @@ fn test_only_headers_with_custom_max_response_bytes_exceeded(env: TestEnv) { let header_size = 142; let max_response_bytes = Some(header_size + n - 1); - let response = block_on(submit_outcall( + let (response, refunded_cycles) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: UnvalidatedCanisterHttpRequestArgs { @@ -1648,7 +1697,7 @@ fn test_only_headers_with_custom_max_response_bytes_exceeded(env: TestEnv) { transform: None, max_response_bytes, }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -1659,6 +1708,9 @@ fn test_only_headers_with_custom_max_response_bytes_exceeded(env: TestEnv) { .. }) ); + assert_ne!( + refunded_cycles, RefundedCycles::Cycles(HTTP_REQUEST_CYCLE_PAYMENT) + ); } fn test_non_ascii_url_is_rejected(env: TestEnv) { @@ -1682,11 +1734,11 @@ fn test_non_ascii_url_is_rejected(env: TestEnv) { max_response_bytes: Some(max_response_bytes), }; - let response = block_on(submit_outcall( + let (response, refunded_cycles) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: request.clone(), - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -1697,6 +1749,10 @@ fn test_non_ascii_url_is_rejected(env: TestEnv) { .. }) ); + + assert_ne!( + refunded_cycles, RefundedCycles::Cycles(HTTP_REQUEST_CYCLE_PAYMENT) + ); } fn test_max_url_length(env: TestEnv) { @@ -1719,14 +1775,14 @@ fn test_max_url_length(env: TestEnv) { max_response_bytes: None, }; - let response = block_on(submit_outcall( + let (response, _) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: request.clone(), - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, - )) - .expect("Request is successful."); + )); + let response = response.expect("Request is successful."); assert_matches!(&response, RemoteHttpResponse {body, status: 200, ..} if *body == expected_body); assert_http_response(&response); @@ -1752,11 +1808,11 @@ fn test_max_url_length_exceeded(env: TestEnv) { max_response_bytes: None, }; - let response = block_on(submit_outcall( + let (response, refunded_cycles) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: request.clone(), - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -1767,6 +1823,9 @@ fn test_max_url_length_exceeded(env: TestEnv) { .. }) ); + assert_eq!( + refunded_cycles, RefundedCycles::Cycles(HTTP_REQUEST_CYCLE_PAYMENT) + ); } fn reference_transform_function_exposed_by_different_canister(env: TestEnv) { @@ -1806,11 +1865,11 @@ fn reference_transform_function_exposed_by_different_canister(env: TestEnv) { }), }; - let response = block_on(submit_outcall( + let (response, _) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: request.clone(), - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -1833,7 +1892,7 @@ fn test_max_number_of_response_headers(env: TestEnv) { webserver_ipv6, "many_response_headers", response_headers ); - let response = block_on(submit_outcall( + let (response, _) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: UnvalidatedCanisterHttpRequestArgs { @@ -1844,10 +1903,10 @@ fn test_max_number_of_response_headers(env: TestEnv) { transform: None, max_response_bytes: None, }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, - )) - .expect("Request is successful."); + )); + let response = response.expect("Request is successful."); assert_matches!(&response, RemoteHttpResponse { status: 200, .. }); assert_http_response(&response); @@ -1870,7 +1929,7 @@ fn test_max_number_of_response_headers_exceeded(env: TestEnv) { webserver_ipv6, "many_response_headers", response_headers ); - let response = block_on(submit_outcall( + let (response, refunded_cycles) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: UnvalidatedCanisterHttpRequestArgs { @@ -1881,7 +1940,7 @@ fn test_max_number_of_response_headers_exceeded(env: TestEnv) { transform: None, max_response_bytes: None, }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); assert_matches!( @@ -1891,6 +1950,9 @@ fn test_max_number_of_response_headers_exceeded(env: TestEnv) { .. }) ); + assert_ne!( + refunded_cycles, RefundedCycles::Cycles(HTTP_REQUEST_CYCLE_PAYMENT) + ); } fn test_max_number_of_request_headers(env: TestEnv) { @@ -1913,10 +1975,10 @@ fn test_max_number_of_request_headers(env: TestEnv) { transform: None, max_response_bytes: None, }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }; - let response = - block_on(submit_outcall(&handlers, request.clone())).expect("Request is successful."); + let (response, _) = block_on(submit_outcall(&handlers, request.clone())); + let response = response.expect("Request is successful."); assert_matches!(&response, RemoteHttpResponse { status: 200, .. }); assert_http_response(&response); @@ -1948,7 +2010,7 @@ fn test_max_number_of_request_headers_exceeded(env: TestEnv) { pub cycles: u64, } - let response = block_on(submit_outcall( + let (response, refunded_cycles) = block_on(submit_outcall( &handlers, TestRemoteHttpRequest { request: TestRequest { @@ -1956,7 +2018,7 @@ fn test_max_number_of_request_headers_exceeded(env: TestEnv) { headers, method: HttpMethod::POST, }, - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, )); @@ -1967,6 +2029,9 @@ fn test_max_number_of_request_headers_exceeded(env: TestEnv) { .. }) ); + assert_eq!( + refunded_cycles, RefundedCycles::Cycles(HTTP_REQUEST_CYCLE_PAYMENT) + ); } fn check_caller_id_on_transform_function(env: TestEnv) { @@ -1992,14 +2057,14 @@ fn check_caller_id_on_transform_function(env: TestEnv) { }), }; - let response = block_on(submit_outcall( + let (response, _) = block_on(submit_outcall( &handlers, RemoteHttpRequest { request: request.clone(), - cycles: 500_000_000_000, + cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, - )) - .expect("Request is successful."); + )); + let response = response.expect("Request is successful."); // Check caller id injected into header. let caller_id = &response @@ -2012,7 +2077,7 @@ fn check_caller_id_on_transform_function(env: TestEnv) { assert_eq!(caller_id, "aaaaa-aa"); } -// ---- END SPEC COMPLIANCE TESTS ------- +// ---- HELPER FUNCTIONS ------- /// Case insensitive header names are distinct. fn assert_distinct_headers(http_response: &RemoteHttpResponse) { @@ -2146,10 +2211,24 @@ fn assert_http_json_response( "4. HTTP bin server received body does not match the outcall sent body." ); } -type ProxyCanisterResponse = Result; -type OutcallsResponse = Result; -async fn submit_outcall(handlers: &Handlers<'_>, request: Request) -> OutcallsResponse +#[derive(Debug, Eq, PartialEq)] +enum RefundedCycles { + NotApplicable, + Cycles(u64), +} + +type ProxyCanisterResponseWithRefund = ResponseWithRefundedCycles; + +// This type represents the result of an IC http_request and the refunded cycles. +// The refund is returned regardless of whether the outcall succeeded (Ok) or failed (Err), +// allowing tests to verify proper cycle refund behavior in both success and error cases. +type OutcallsResponseWithRefund = (Result, RefundedCycles); + +async fn submit_outcall( + handlers: &Handlers<'_>, + request: Request, +) -> OutcallsResponseWithRefund where Request: Clone + CandidType, { @@ -2159,20 +2238,31 @@ where let principal_id: PrincipalId = handlers.proxy_canister().effective_canister_id(); let principal: Principal = principal_id.into(); - agent - .update(&principal, "send_request") + let canister_response = agent + .update(&principal, "send_request_and_retrieve_refund") .with_arg(args) .call_and_wait() - .await - .map_err(|agent_error| match agent_error { - AgentError::CertifiedReject(response) | AgentError::UncertifiedReject(response) => { - response - } - _ => panic!("Unexpected error: {:?}", agent_error), - }) - .and_then(|response| { - decode_one::(&response) - .unwrap() + .await; + + match canister_response { + Err(agent_error) => { + let err_resp = match agent_error { + AgentError::CertifiedReject(response) | AgentError::UncertifiedReject(response) => { + response + } + _ => panic!("Unexpected error: {:?}", agent_error), + }; + // The + (Err(err_resp), RefundedCycles::NotApplicable) + } + Ok(serialized_bytes) => { + let response_with_refund = + decode_one::(&serialized_bytes) + .expect("Decoding the canister serialized response should succeed."); + + let refunded_cycles = response_with_refund.refunded_cycles; + let result = response_with_refund + .result .map_err(|(reject_code, reject_message)| { let reject_code = match reject_code { RejectionCode::SysFatal => RejectCode::SysFatal, @@ -2190,8 +2280,10 @@ where reject_message, error_code: None, } - }) - }) + }); + (result, RefundedCycles::Cycles(refunded_cycles)) + } + } } /// Pricing function of canister http requests.