diff --git a/canhttp/src/client/mod.rs b/canhttp/src/client/mod.rs index 4cdf326..aeb14c3 100644 --- a/canhttp/src/client/mod.rs +++ b/canhttp/src/client/mod.rs @@ -178,6 +178,34 @@ 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. + fn set_is_replicated(&mut self, value: bool); + + /// Returns the replication mode of the request, if explicitly set. + fn get_is_replicated(&self) -> Option; + + /// Sets the replication mode using the builder pattern. + fn 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..10413d6 100644 --- a/canhttp/src/http/tests.rs +++ b/canhttp/src/http/tests.rs @@ -4,7 +4,7 @@ use crate::{ response::{HttpResponse, HttpResponseConversionError}, HttpConversionLayer, HttpRequestConverter, HttpResponseConverter, }, - ConvertServiceBuilder, IcError, MaxResponseBytesRequestExtension, + ConvertServiceBuilder, IcError, IsReplicatedRequestExtension, MaxResponseBytesRequestExtension, TransformContextRequestExtension, }; use assert_matches::assert_matches; @@ -27,6 +27,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 +42,7 @@ async fn should_convert_http_request() { let request = request_builder .max_response_bytes(max_response_bytes) .transform_context(transform_context.clone()) + .replicated(is_replicated) .header("Content-Type", "application/json") .body(body.clone()) .unwrap(); @@ -59,12 +61,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) + .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 +207,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()) + .replicated(is_replicated) .header("Content-Type", "application/json") .body(body.clone()) .unwrap(); @@ -209,7 +232,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..4432714 100644 --- a/canhttp/src/lib.rs +++ b/canhttp/src/lib.rs @@ -6,8 +6,8 @@ #![forbid(missing_docs)] pub use client::{ - Client, HttpsOutcallError, IcError, MaxResponseBytesRequestExtension, - TransformContextRequestExtension, + Client, HttpsOutcallError, IcError, IsReplicatedRequestExtension, + MaxResponseBytesRequestExtension, TransformContextRequestExtension, }; pub use convert::ConvertServiceBuilder;