From af963bfcc7cd5900e9931f3940ca08d7c000dcb7 Mon Sep 17 00:00:00 2001 From: Franz-Stefan Preiss Date: Sun, 1 Mar 2026 00:19:02 +0000 Subject: [PATCH 1/4] feat(flexible-outcalls): IC-1949 add error types for flexible outcalls --- .../management_canister_types/src/http.rs | 149 ++++++++++++++++++ rs/types/management_canister_types/src/lib.rs | 7 +- 2 files changed, 153 insertions(+), 3 deletions(-) diff --git a/rs/types/management_canister_types/src/http.rs b/rs/types/management_canister_types/src/http.rs index 4de5e3baf1a5..c111ca5e2c3b 100644 --- a/rs/types/management_canister_types/src/http.rs +++ b/rs/types/management_canister_types/src/http.rs @@ -391,3 +391,152 @@ pub struct CanisterHttpResponsePayload { } impl Payload<'_> for CanisterHttpResponsePayload {} + +/// The result type for the `flexible_http_request` management canister endpoint. +/// +/// ```text +/// type flexible_http_request_result = variant { +/// ok: vec http_request_result; +/// err: flexible_http_request_err; +/// }; +/// ``` +#[derive(Clone, Eq, PartialEq, Hash, Debug, CandidType, Deserialize, Serialize)] +pub enum FlexibleHttpRequestResult { + #[serde(rename = "ok")] + Ok(Vec), + #[serde(rename = "err")] + Err(FlexibleHttpRequestErr), +} + +/// The error type returned by `flexible_http_request`. +/// +/// ```text +/// type flexible_http_request_err = record { +/// global_error: opt variant { ... }; +/// node_details : vec record { ... }; +/// message: text; +/// }; +/// ``` +#[derive(Clone, Eq, PartialEq, Hash, Debug, CandidType, Deserialize, Serialize)] +pub struct FlexibleHttpRequestErr { + pub global_error: Option, + pub node_details: Vec, + pub message: String, +} + +/// Why the flexible HTTP outcall failed globally. +#[derive(Clone, Eq, PartialEq, Hash, Debug, CandidType, Deserialize, Serialize)] +pub enum FlexibleHttpGlobalError { + #[serde(rename = "invalid_parameters")] + InvalidParameters, + #[serde(rename = "timeout")] + Timeout, + #[serde(rename = "out_of_cycles")] + OutOfCycles, + #[serde(rename = "responses_too_large")] + ResponsesTooLarge, +} + +/// Per-node detail in a flexible HTTP outcall error. +/// +/// ```text +/// record { +/// node_id: principal; +/// report: http_request_resource_report; +/// error: opt record { code: text; message: text }; +/// } +/// ``` +#[derive(Clone, Eq, PartialEq, Hash, Debug, CandidType, Deserialize, Serialize)] +pub struct FlexibleHttpNodeDetail { + pub node_id: candid::Principal, + pub report: HttpRequestResourceReport, + pub error: Option, +} + +/// Per-node resource usage accounting for an HTTP outcall. +/// +/// ```text +/// type http_request_resource_report = record { +/// raw_response_bytes: opt variant { used: nat64; exceeded: reserved }; +/// http_roundtrip_time_ms: opt variant { used: nat64; exceeded: reserved }; +/// transform_instructions: opt variant { used: nat64; exceeded: reserved }; +/// transformed_response_bytes: opt variant { used: nat64; exceeded: reserved }; +/// cycles: opt variant { used: nat; exceeded: reserved }; +/// }; +/// ``` +#[derive(Clone, Eq, PartialEq, Hash, Debug, CandidType, Deserialize, Serialize)] +pub struct HttpRequestResourceReport { + pub raw_response_bytes: Option>, + pub http_roundtrip_time_ms: Option>, + pub transform_instructions: Option>, + pub transformed_response_bytes: Option>, + pub cycles: Option>, +} + +/// Tracks whether a resource was used or exceeded its budget. +#[derive(Clone, Eq, PartialEq, Hash, Debug, CandidType, Deserialize, Serialize)] +pub enum ResourceUsage { + #[serde(rename = "used")] + Used(T), + #[serde(rename = "exceeded")] + Exceeded, +} + +/// Error details from a specific node during a flexible HTTP outcall. +#[derive(Clone, Eq, PartialEq, Hash, Debug, CandidType, Deserialize, Serialize)] +pub struct FlexibleHttpNodeError { + pub code: String, + pub message: String, +} + +#[cfg(test)] +mod flexible_result_tests { + use super::*; + use candid::{Decode, Encode, Nat}; + + fn roundtrip(original: &FlexibleHttpRequestResult) -> FlexibleHttpRequestResult { + let bytes = Encode!(original).unwrap(); + Decode!(&bytes, FlexibleHttpRequestResult).unwrap() + } + + fn sample_response(status: u128) -> CanisterHttpResponsePayload { + CanisterHttpResponsePayload { + status, + headers: vec![HttpHeader { + name: "content-type".to_string(), + value: "text/plain".to_string(), + }], + body: b"hello".to_vec(), + } + } + + #[test] + fn ok_with_multiple_responses() { + let original = + FlexibleHttpRequestResult::Ok(vec![sample_response(200), sample_response(404)]); + assert_eq!(roundtrip(&original), original); + } + + #[test] + fn err_roundtrip_with_nested_fields() { + let original = FlexibleHttpRequestResult::Err(FlexibleHttpRequestErr { + global_error: Some(FlexibleHttpGlobalError::Timeout), + node_details: vec![FlexibleHttpNodeDetail { + node_id: candid::Principal::from_slice(&[1, 2, 3]), + report: HttpRequestResourceReport { + raw_response_bytes: Some(ResourceUsage::Used(1024)), + http_roundtrip_time_ms: Some(ResourceUsage::Used(200)), + transform_instructions: Some(ResourceUsage::Exceeded), + transformed_response_bytes: None, + cycles: Some(ResourceUsage::Used(Nat::from(500_000u64))), + }, + error: Some(FlexibleHttpNodeError { + code: "CONNECTION_REFUSED".to_string(), + message: "connection refused by remote".to_string(), + }), + }], + message: "partial failure".to_string(), + }); + assert_eq!(roundtrip(&original), original); + } +} diff --git a/rs/types/management_canister_types/src/lib.rs b/rs/types/management_canister_types/src/lib.rs index adf7c622d2ef..d0e079d175bf 100644 --- a/rs/types/management_canister_types/src/lib.rs +++ b/rs/types/management_canister_types/src/lib.rs @@ -12,9 +12,10 @@ pub use data_size::*; pub use http::{ ALLOWED_HTTP_OUTCALLS_PRICING_VERSIONS, BoundedHttpHeaders, CanisterHttpRequestArgs, CanisterHttpResponsePayload, DEFAULT_HTTP_OUTCALLS_PRICING_VERSION, - FlexibleCanisterHttpRequestArgs, HttpHeader, HttpMethod, PRICING_VERSION_LEGACY, - PRICING_VERSION_PAY_AS_YOU_GO, ReplicationCounts, TransformArgs, TransformContext, - TransformFunc, + FlexibleCanisterHttpRequestArgs, FlexibleHttpGlobalError, FlexibleHttpNodeDetail, + FlexibleHttpNodeError, FlexibleHttpRequestErr, FlexibleHttpRequestResult, HttpHeader, + HttpMethod, HttpRequestResourceReport, PRICING_VERSION_LEGACY, PRICING_VERSION_PAY_AS_YOU_GO, + ReplicationCounts, ResourceUsage, TransformArgs, TransformContext, TransformFunc, }; use ic_base_types::{ CanisterId, EnvironmentVariables, NodeId, NumBytes, PrincipalId, RegistryVersion, SnapshotId, From f1c4052a04e2c525f9d98b56534aa658f5ba3e2a Mon Sep 17 00:00:00 2001 From: Franz-Stefan Preiss Date: Mon, 2 Mar 2026 10:12:00 +0000 Subject: [PATCH 2/4] remove tests: they actually don't seem useful --- .../management_canister_types/src/http.rs | 54 +------------------ 1 file changed, 1 insertion(+), 53 deletions(-) diff --git a/rs/types/management_canister_types/src/http.rs b/rs/types/management_canister_types/src/http.rs index c111ca5e2c3b..322823576ee6 100644 --- a/rs/types/management_canister_types/src/http.rs +++ b/rs/types/management_canister_types/src/http.rs @@ -487,56 +487,4 @@ pub enum ResourceUsage { pub struct FlexibleHttpNodeError { pub code: String, pub message: String, -} - -#[cfg(test)] -mod flexible_result_tests { - use super::*; - use candid::{Decode, Encode, Nat}; - - fn roundtrip(original: &FlexibleHttpRequestResult) -> FlexibleHttpRequestResult { - let bytes = Encode!(original).unwrap(); - Decode!(&bytes, FlexibleHttpRequestResult).unwrap() - } - - fn sample_response(status: u128) -> CanisterHttpResponsePayload { - CanisterHttpResponsePayload { - status, - headers: vec![HttpHeader { - name: "content-type".to_string(), - value: "text/plain".to_string(), - }], - body: b"hello".to_vec(), - } - } - - #[test] - fn ok_with_multiple_responses() { - let original = - FlexibleHttpRequestResult::Ok(vec![sample_response(200), sample_response(404)]); - assert_eq!(roundtrip(&original), original); - } - - #[test] - fn err_roundtrip_with_nested_fields() { - let original = FlexibleHttpRequestResult::Err(FlexibleHttpRequestErr { - global_error: Some(FlexibleHttpGlobalError::Timeout), - node_details: vec![FlexibleHttpNodeDetail { - node_id: candid::Principal::from_slice(&[1, 2, 3]), - report: HttpRequestResourceReport { - raw_response_bytes: Some(ResourceUsage::Used(1024)), - http_roundtrip_time_ms: Some(ResourceUsage::Used(200)), - transform_instructions: Some(ResourceUsage::Exceeded), - transformed_response_bytes: None, - cycles: Some(ResourceUsage::Used(Nat::from(500_000u64))), - }, - error: Some(FlexibleHttpNodeError { - code: "CONNECTION_REFUSED".to_string(), - message: "connection refused by remote".to_string(), - }), - }], - message: "partial failure".to_string(), - }); - assert_eq!(roundtrip(&original), original); - } -} +} \ No newline at end of file From e4a8c124d0c4606c7f956b51da65a45bebb7adf9 Mon Sep 17 00:00:00 2001 From: Franz-Stefan Preiss Date: Mon, 2 Mar 2026 10:12:55 +0000 Subject: [PATCH 3/4] fmt --- rs/types/management_canister_types/src/http.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/types/management_canister_types/src/http.rs b/rs/types/management_canister_types/src/http.rs index 322823576ee6..949f49fdfdac 100644 --- a/rs/types/management_canister_types/src/http.rs +++ b/rs/types/management_canister_types/src/http.rs @@ -487,4 +487,4 @@ pub enum ResourceUsage { pub struct FlexibleHttpNodeError { pub code: String, pub message: String, -} \ No newline at end of file +} From d0c694e7a65ff96f1252f32f909bfc962c9df371 Mon Sep 17 00:00:00 2001 From: Franz-Stefan Preiss Date: Tue, 3 Mar 2026 15:16:44 +0000 Subject: [PATCH 4/4] use candid::Reserved for future extensibility --- rs/types/management_canister_types/src/http.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rs/types/management_canister_types/src/http.rs b/rs/types/management_canister_types/src/http.rs index 949f49fdfdac..a7d7753981d3 100644 --- a/rs/types/management_canister_types/src/http.rs +++ b/rs/types/management_canister_types/src/http.rs @@ -428,13 +428,13 @@ pub struct FlexibleHttpRequestErr { #[derive(Clone, Eq, PartialEq, Hash, Debug, CandidType, Deserialize, Serialize)] pub enum FlexibleHttpGlobalError { #[serde(rename = "invalid_parameters")] - InvalidParameters, + InvalidParameters(candid::Reserved), #[serde(rename = "timeout")] - Timeout, + Timeout(candid::Reserved), #[serde(rename = "out_of_cycles")] - OutOfCycles, + OutOfCycles(candid::Reserved), #[serde(rename = "responses_too_large")] - ResponsesTooLarge, + ResponsesTooLarge(candid::Reserved), } /// Per-node detail in a flexible HTTP outcall error. @@ -479,7 +479,7 @@ pub enum ResourceUsage { #[serde(rename = "used")] Used(T), #[serde(rename = "exceeded")] - Exceeded, + Exceeded(candid::Reserved), } /// Error details from a specific node during a flexible HTTP outcall.