From 51bf4aaf5d92b8c79b54f7eef5933f61f91d09df Mon Sep 17 00:00:00 2001 From: clankpan Date: Mon, 26 Jan 2026 23:38:26 +0900 Subject: [PATCH 1/6] feat: add is_replicated request extension --- canhttp/src/client/mod.rs | 25 +++++++++++++++++++++++++ canhttp/src/http/request.rs | 37 +++++++++++++++++++++++++++++++++++-- canhttp/src/http/tests.rs | 30 +++++++++++++++++++++++++++--- canhttp/src/lib.rs | 3 ++- 4 files changed, 89 insertions(+), 6 deletions(-) diff --git a/canhttp/src/client/mod.rs b/canhttp/src/client/mod.rs index 4cdf326..33b9fa3 100644 --- a/canhttp/src/client/mod.rs +++ b/canhttp/src/client/mod.rs @@ -178,6 +178,31 @@ impl TransformContextRequestExtension for IcHttpRequest { } } +/// Add support for selecting replicated or non-replicated HTTP outcalls. +pub trait IsReplicatedRequestExtension: Sized { + /// Set whether the request should be executed as replicated or non-replicated. + fn set_is_replicated(&mut self, value: bool); + + /// Retrieve the current is_replicated value, if any. + fn get_is_replicated(&self) -> Option; + + /// Convenience method to use the builder pattern. + fn is_replicated(mut self, value: bool) -> Self { + self.set_is_replicated(value); + self + } +} + +impl IsReplicatedRequestExtension for IcHttpRequest { + fn set_is_replicated(&mut self, value: bool) { + self.is_replicated = Some(value); + } + + fn get_is_replicated(&self) -> Option { + self.is_replicated + } +} + /// Characterize errors that are specific to HTTPs outcalls. pub trait HttpsOutcallError { /// Determines whether the error indicates that the response was larger than the specified diff --git a/canhttp/src/http/request.rs b/canhttp/src/http/request.rs index c295fa6..d0d1676 100644 --- a/canhttp/src/http/request.rs +++ b/canhttp/src/http/request.rs @@ -1,5 +1,8 @@ use crate::convert::Convert; -use crate::{MaxResponseBytesRequestExtension, TransformContextRequestExtension}; +use crate::{ + IsReplicatedRequestExtension, MaxResponseBytesRequestExtension, + TransformContextRequestExtension, +}; use ic_cdk::management_canister::{ HttpHeader as IcHttpHeader, HttpMethod as IcHttpMethod, HttpRequestArgs as IcHttpRequest, TransformContext, @@ -67,6 +70,35 @@ impl TransformContextRequestExtension for http::request::Builder { } } +#[derive(Clone, Debug, PartialEq, Eq)] +struct IsReplicatedExtension(pub bool); + +impl IsReplicatedRequestExtension for http::Request { + fn set_is_replicated(&mut self, value: bool) { + let extensions = self.extensions_mut(); + extensions.insert(IsReplicatedExtension(value)); + } + + fn get_is_replicated(&self) -> Option { + self.extensions() + .get::() + .map(|e| e.0) + } +} + +impl IsReplicatedRequestExtension for http::request::Builder { + fn set_is_replicated(&mut self, value: bool) { + if let Some(extensions) = self.extensions_mut() { + extensions.insert(IsReplicatedExtension(value)); + } + } + + fn get_is_replicated(&self) -> Option { + self.extensions_ref() + .and_then(|extensions| extensions.get::().map(|e| e.0)) + } +} + /// Error return when converting requests with [`HttpRequestConverter`]. #[derive(Error, Clone, Debug, Eq, PartialEq)] pub enum HttpRequestConversionError { @@ -119,6 +151,7 @@ impl Convert for HttpRequestConverter { }) .collect::, _>>()?; let transform = request.get_transform_context().cloned(); + let is_replicated = request.get_is_replicated(); let body = Some(request.into_body()); Ok(IcHttpRequest { url, @@ -127,7 +160,7 @@ impl Convert for HttpRequestConverter { headers, body, transform, - is_replicated: None, + is_replicated, }) } } diff --git a/canhttp/src/http/tests.rs b/canhttp/src/http/tests.rs index e10071f..506ce07 100644 --- a/canhttp/src/http/tests.rs +++ b/canhttp/src/http/tests.rs @@ -4,7 +4,8 @@ use crate::{ response::{HttpResponse, HttpResponseConversionError}, HttpConversionLayer, HttpRequestConverter, HttpResponseConverter, }, - ConvertServiceBuilder, IcError, MaxResponseBytesRequestExtension, + ConvertServiceBuilder, IcError, IsReplicatedRequestExtension, + MaxResponseBytesRequestExtension, TransformContextRequestExtension, }; use assert_matches::assert_matches; @@ -27,6 +28,7 @@ async fn should_convert_http_request() { function: TransformFunc::new(Principal::management_canister(), "sanitize".to_string()), context: vec![35_u8; 20], }; + let is_replicated = true; let body = vec![42_u8; 32]; let mut service = ServiceBuilder::new() @@ -41,6 +43,7 @@ async fn should_convert_http_request() { let request = request_builder .max_response_bytes(max_response_bytes) .transform_context(transform_context.clone()) + .is_replicated(is_replicated) .header("Content-Type", "application/json") .body(body.clone()) .unwrap(); @@ -59,12 +62,31 @@ async fn should_convert_http_request() { }], body: Some(body.clone()), transform: Some(transform_context.clone()), - is_replicated: None, + is_replicated: Some(is_replicated), } ) } } +#[tokio::test] +async fn should_convert_is_replicated_flag() { + let url = "https://internetcomputer.org/"; + let mut service = ServiceBuilder::new() + .convert_request(HttpRequestConverter) + .service_fn(echo_request); + + for is_replicated in [true, false] { + let request = http::Request::get(url) + .is_replicated(is_replicated) + .body(vec![]) + .unwrap(); + + let converted_request = service.ready().await.unwrap().call(request).await.unwrap(); + + assert_eq!(converted_request.is_replicated, Some(is_replicated)); + } +} + #[tokio::test] async fn should_fail_when_http_method_unsupported() { let mut service = ServiceBuilder::new() @@ -186,10 +208,12 @@ async fn should_convert_both_request_and_responses() { function: TransformFunc::new(Principal::management_canister(), "sanitize".to_string()), context: vec![35_u8; 20], }; + let is_replicated = false; let body = vec![42_u8; 32]; let request = http::Request::post(url) .max_response_bytes(max_response_bytes) .transform_context(transform_context.clone()) + .is_replicated(is_replicated) .header("Content-Type", "application/json") .body(body.clone()) .unwrap(); @@ -209,7 +233,7 @@ async fn should_convert_both_request_and_responses() { }], body: Some(body.clone()), transform: Some(transform_context.clone()), - is_replicated: None, + is_replicated: Some(is_replicated), } ); diff --git a/canhttp/src/lib.rs b/canhttp/src/lib.rs index 8641b26..1565be2 100644 --- a/canhttp/src/lib.rs +++ b/canhttp/src/lib.rs @@ -6,7 +6,8 @@ #![forbid(missing_docs)] pub use client::{ - Client, HttpsOutcallError, IcError, MaxResponseBytesRequestExtension, + Client, HttpsOutcallError, IcError, IsReplicatedRequestExtension, + MaxResponseBytesRequestExtension, TransformContextRequestExtension, }; pub use convert::ConvertServiceBuilder; From 39320a8fd7ef493a7ac2ff0f232e36580f314694 Mon Sep 17 00:00:00 2001 From: clankpan Date: Tue, 27 Jan 2026 11:27:21 +0900 Subject: [PATCH 2/6] fix: rename is_replicated builder to replicated --- canhttp/src/client/mod.rs | 2 +- canhttp/src/http/tests.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/canhttp/src/client/mod.rs b/canhttp/src/client/mod.rs index 33b9fa3..923ac36 100644 --- a/canhttp/src/client/mod.rs +++ b/canhttp/src/client/mod.rs @@ -187,7 +187,7 @@ pub trait IsReplicatedRequestExtension: Sized { fn get_is_replicated(&self) -> Option; /// Convenience method to use the builder pattern. - fn is_replicated(mut self, value: bool) -> Self { + fn replicated(mut self, value: bool) -> Self { self.set_is_replicated(value); self } diff --git a/canhttp/src/http/tests.rs b/canhttp/src/http/tests.rs index 506ce07..3f0f321 100644 --- a/canhttp/src/http/tests.rs +++ b/canhttp/src/http/tests.rs @@ -43,7 +43,7 @@ async fn should_convert_http_request() { let request = request_builder .max_response_bytes(max_response_bytes) .transform_context(transform_context.clone()) - .is_replicated(is_replicated) + .replicated(is_replicated) .header("Content-Type", "application/json") .body(body.clone()) .unwrap(); @@ -77,7 +77,7 @@ async fn should_convert_is_replicated_flag() { for is_replicated in [true, false] { let request = http::Request::get(url) - .is_replicated(is_replicated) + .replicated(is_replicated) .body(vec![]) .unwrap(); @@ -213,7 +213,7 @@ async fn should_convert_both_request_and_responses() { let request = http::Request::post(url) .max_response_bytes(max_response_bytes) .transform_context(transform_context.clone()) - .is_replicated(is_replicated) + .replicated(is_replicated) .header("Content-Type", "application/json") .body(body.clone()) .unwrap(); From 34202031e53663e7e83f08cc533c24750e55ca1c Mon Sep 17 00:00:00 2001 From: ClankPan <87867925+ClankPan@users.noreply.github.com> Date: Wed, 28 Jan 2026 11:31:45 +0900 Subject: [PATCH 3/6] Update canhttp/src/client/mod.rs Co-authored-by: Louis Pahlavi --- canhttp/src/client/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/canhttp/src/client/mod.rs b/canhttp/src/client/mod.rs index 923ac36..e161de3 100644 --- a/canhttp/src/client/mod.rs +++ b/canhttp/src/client/mod.rs @@ -183,7 +183,7 @@ pub trait IsReplicatedRequestExtension: Sized { /// Set whether the request should be executed as replicated or non-replicated. fn set_is_replicated(&mut self, value: bool); - /// Retrieve the current is_replicated value, if any. + /// Returns the replication mode of the request, if explicitly set. fn get_is_replicated(&self) -> Option; /// Convenience method to use the builder pattern. From 66ae507521bd3e62b5ffa205c69e28315fe77e91 Mon Sep 17 00:00:00 2001 From: ClankPan <87867925+ClankPan@users.noreply.github.com> Date: Wed, 28 Jan 2026 11:32:07 +0900 Subject: [PATCH 4/6] Update canhttp/src/client/mod.rs Co-authored-by: Louis Pahlavi --- canhttp/src/client/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/canhttp/src/client/mod.rs b/canhttp/src/client/mod.rs index e161de3..e1a542e 100644 --- a/canhttp/src/client/mod.rs +++ b/canhttp/src/client/mod.rs @@ -186,7 +186,7 @@ pub trait IsReplicatedRequestExtension: Sized { /// Returns the replication mode of the request, if explicitly set. fn get_is_replicated(&self) -> Option; - /// Convenience method to use the builder pattern. + /// Sets the replication mode using the builder pattern. fn replicated(mut self, value: bool) -> Self { self.set_is_replicated(value); self From 5de12d29ed4fc35d236f6f0e8c9d65245deee4a6 Mon Sep 17 00:00:00 2001 From: clankpan Date: Wed, 28 Jan 2026 11:44:13 +0900 Subject: [PATCH 5/6] docs: add warning and link for replication mode --- canhttp/src/client/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/canhttp/src/client/mod.rs b/canhttp/src/client/mod.rs index e1a542e..c8fd88f 100644 --- a/canhttp/src/client/mod.rs +++ b/canhttp/src/client/mod.rs @@ -179,8 +179,11 @@ impl TransformContextRequestExtension for IcHttpRequest { } /// Add support for selecting replicated or non-replicated HTTP outcalls. +/// +/// Warning: non-replicated outcalls are currently experimental. +/// See the [docs](https://docs.internetcomputer.org/references/ic-interface-spec/#ic-http_request) for more ditails. pub trait IsReplicatedRequestExtension: Sized { - /// Set whether the request should be executed as replicated or non-replicated. + /// Set the request replication mode. fn set_is_replicated(&mut self, value: bool); /// Returns the replication mode of the request, if explicitly set. From 0e0fd14d5be8dab6cd05c0eae7ce79b44aeff1f8 Mon Sep 17 00:00:00 2001 From: clankpan Date: Wed, 28 Jan 2026 11:47:25 +0900 Subject: [PATCH 6/6] chore: format --- canhttp/src/client/mod.rs | 4 ++-- canhttp/src/http/tests.rs | 3 +-- canhttp/src/lib.rs | 3 +-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/canhttp/src/client/mod.rs b/canhttp/src/client/mod.rs index c8fd88f..aeb14c3 100644 --- a/canhttp/src/client/mod.rs +++ b/canhttp/src/client/mod.rs @@ -179,11 +179,11 @@ impl TransformContextRequestExtension for IcHttpRequest { } /// Add support for selecting replicated or non-replicated HTTP outcalls. -/// +/// /// Warning: non-replicated outcalls are currently experimental. /// See the [docs](https://docs.internetcomputer.org/references/ic-interface-spec/#ic-http_request) for more ditails. pub trait IsReplicatedRequestExtension: Sized { - /// Set the request replication mode. + /// Set the request replication mode. fn set_is_replicated(&mut self, value: bool); /// Returns the replication mode of the request, if explicitly set. diff --git a/canhttp/src/http/tests.rs b/canhttp/src/http/tests.rs index 3f0f321..10413d6 100644 --- a/canhttp/src/http/tests.rs +++ b/canhttp/src/http/tests.rs @@ -4,8 +4,7 @@ use crate::{ response::{HttpResponse, HttpResponseConversionError}, HttpConversionLayer, HttpRequestConverter, HttpResponseConverter, }, - ConvertServiceBuilder, IcError, IsReplicatedRequestExtension, - MaxResponseBytesRequestExtension, + ConvertServiceBuilder, IcError, IsReplicatedRequestExtension, MaxResponseBytesRequestExtension, TransformContextRequestExtension, }; use assert_matches::assert_matches; diff --git a/canhttp/src/lib.rs b/canhttp/src/lib.rs index 1565be2..4432714 100644 --- a/canhttp/src/lib.rs +++ b/canhttp/src/lib.rs @@ -7,8 +7,7 @@ pub use client::{ Client, HttpsOutcallError, IcError, IsReplicatedRequestExtension, - MaxResponseBytesRequestExtension, - TransformContextRequestExtension, + MaxResponseBytesRequestExtension, TransformContextRequestExtension, }; pub use convert::ConvertServiceBuilder;