canic-core 0.35.7

Canic — a canister orchestration and management toolkit for the Internet Computer
Documentation
use crate::{
    InternalError,
    ids::SystemMetricKind,
    infra::{InfraError, ic::http::HttpInfra},
    ops::{
        ic::IcOpsError,
        runtime::metrics::{
            http::{HttpMethod as MetricsHttpMethod, HttpMetrics},
            platform_call::{
                PlatformCallMetricMode, PlatformCallMetricOutcome, PlatformCallMetricReason,
                PlatformCallMetricSurface, PlatformCallMetrics,
            },
            system::SystemMetrics,
        },
    },
};
use candid::Nat;
use thiserror::Error as ThisError;

///
/// Http
/// Approved, observable HTTP helpers over the IC management API.
///

/// Maximum allowed response size for HTTP outcalls.
pub const MAX_RESPONSE_BYTES: u64 = 200_000;

///
/// HttpHeader
///

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HttpHeader {
    pub name: String,
    pub value: String,
}

///
/// HttpMethod
///

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HttpMethod {
    Get,
    Head,
    Post,
}

///
/// HttpRequestArgs
///

#[derive(Clone, Debug)]
pub struct HttpRequestArgs {
    pub url: String,
    pub max_response_bytes: Option<u64>,
    pub method: HttpMethod,
    pub headers: Vec<HttpHeader>,
    pub body: Option<Vec<u8>>,
    pub is_replicated: Option<bool>,
}

impl Default for HttpRequestArgs {
    fn default() -> Self {
        Self {
            url: String::new(),
            max_response_bytes: None,
            method: HttpMethod::Get,
            headers: Vec::new(),
            body: None,
            is_replicated: None,
        }
    }
}

///
/// HttpRequestResult
///

#[derive(Clone, Debug)]
pub struct HttpRequestResult {
    pub status: Nat,
    pub headers: Vec<HttpHeader>,
    pub body: Vec<u8>,
}

///
/// HttpOpsError
///

#[derive(Debug, ThisError)]
pub enum HttpOpsError {
    #[error("http error status: {0}")]
    HttpStatus(u32),

    #[error(transparent)]
    Infra(#[from] InfraError),
}

impl From<HttpOpsError> for InternalError {
    fn from(err: HttpOpsError) -> Self {
        Self::from(IcOpsError::from(err))
    }
}

///
/// HttpOps
///

pub struct HttpOps;

impl HttpOps {
    // -------------------------------------------------------------------------
    // High-level helpers
    // -------------------------------------------------------------------------

    /// Perform an HTTP GET request and return the raw response.
    /// Prefer `get_with_label` for dynamic URLs to keep HTTP metrics bounded.
    pub async fn get(
        url: &str,
        headers: &[(&str, &str)],
    ) -> Result<HttpRequestResult, InternalError> {
        Self::get_with_label(url, headers, None).await
    }

    /// Same as `get`, with an optional metrics label.
    /// The label should be stable and low-cardinality when provided.
    pub async fn get_with_label(
        url: &str,
        headers: &[(&str, &str)],
        label: Option<&str>,
    ) -> Result<HttpRequestResult, InternalError> {
        let args = HttpRequestArgs {
            url: url.to_string(),
            method: HttpMethod::Get,
            headers: Self::headers_from_pairs(headers),
            max_response_bytes: Some(MAX_RESPONSE_BYTES),
            ..Default::default()
        };

        let res = Self::perform_request(args, label).await?;
        let status = u32::try_from(&res.status.0).unwrap_or(u32::MAX);

        if !(200..300).contains(&status) {
            record_http_call(
                PlatformCallMetricOutcome::Failed,
                PlatformCallMetricReason::HttpStatus,
            );
            return Err(HttpOpsError::HttpStatus(status).into());
        }

        Ok(res)
    }

    // -------------------------------------------------------------------------
    // Low-level escape hatches
    // -------------------------------------------------------------------------

    /// Same as `get_raw`, with an optional metrics label.
    /// Pass a label when raw URLs can contain IDs, timestamps, or other unbounded values.
    pub async fn get_raw_with_label(
        args: HttpRequestArgs,
        label: Option<&str>,
    ) -> Result<HttpRequestResult, InternalError> {
        Self::perform_request(args, label).await
    }

    // -------------------------------------------------------------------------
    // Core execution
    // -------------------------------------------------------------------------

    /// Perform a raw IC HTTP outcall with mandatory metrics.
    async fn perform_request(
        args: HttpRequestArgs,
        label: Option<&str>,
    ) -> Result<HttpRequestResult, InternalError> {
        Self::record_metrics(args.method, &args.url, label);
        let cdk_args = crate::cdk::mgmt::HttpRequestArgs::from(args);
        record_http_call(
            PlatformCallMetricOutcome::Started,
            PlatformCallMetricReason::Ok,
        );
        let res = match HttpInfra::http_request_raw(&cdk_args).await {
            Ok(res) => res,
            Err(err) => {
                record_http_call(
                    PlatformCallMetricOutcome::Failed,
                    PlatformCallMetricReason::Infra,
                );
                return Err(HttpOpsError::from(err).into());
            }
        };
        record_http_call(
            PlatformCallMetricOutcome::Completed,
            PlatformCallMetricReason::Ok,
        );

        Ok(HttpRequestResult::from(res))
    }

    /// Record outbound HTTP metrics.
    /// URL-derived fallback labels strip query/fragment only; callers own path cardinality.
    fn record_metrics(method: HttpMethod, url: &str, label: Option<&str>) {
        SystemMetrics::increment(SystemMetricKind::HttpOutcall);
        HttpMetrics::record_http_request(metrics_method(method), url, label);
    }

    ///
    /// helpers
    ///

    fn headers_from_pairs(headers: &[(&str, &str)]) -> Vec<HttpHeader> {
        headers
            .iter()
            .map(|(name, value)| HttpHeader {
                name: (*name).to_string(),
                value: (*value).to_string(),
            })
            .collect()
    }
}

// Record one HTTP outcall metric with no URL or label dimensions.
fn record_http_call(outcome: PlatformCallMetricOutcome, reason: PlatformCallMetricReason) {
    PlatformCallMetrics::record(
        PlatformCallMetricSurface::Http,
        PlatformCallMetricMode::Update,
        outcome,
        reason,
    );
}

const fn metrics_method(method: HttpMethod) -> MetricsHttpMethod {
    match method {
        HttpMethod::Get => MetricsHttpMethod::Get,
        HttpMethod::Post => MetricsHttpMethod::Post,
        HttpMethod::Head => MetricsHttpMethod::Head,
    }
}

impl From<HttpMethod> for crate::cdk::mgmt::HttpMethod {
    fn from(method: HttpMethod) -> Self {
        match method {
            HttpMethod::Get => Self::GET,
            HttpMethod::Post => Self::POST,
            HttpMethod::Head => Self::HEAD,
        }
    }
}

impl From<crate::cdk::mgmt::HttpMethod> for HttpMethod {
    fn from(method: crate::cdk::mgmt::HttpMethod) -> Self {
        match method {
            crate::cdk::mgmt::HttpMethod::GET => Self::Get,
            crate::cdk::mgmt::HttpMethod::POST => Self::Post,
            crate::cdk::mgmt::HttpMethod::HEAD => Self::Head,
        }
    }
}

impl From<HttpHeader> for crate::cdk::mgmt::HttpHeader {
    fn from(header: HttpHeader) -> Self {
        Self {
            name: header.name,
            value: header.value,
        }
    }
}

impl From<crate::cdk::mgmt::HttpHeader> for HttpHeader {
    fn from(header: crate::cdk::mgmt::HttpHeader) -> Self {
        Self {
            name: header.name,
            value: header.value,
        }
    }
}

impl From<HttpRequestArgs> for crate::cdk::mgmt::HttpRequestArgs {
    fn from(args: HttpRequestArgs) -> Self {
        Self {
            url: args.url,
            max_response_bytes: args.max_response_bytes,
            method: args.method.into(),
            headers: args.headers.into_iter().map(Into::into).collect(),
            body: args.body,
            transform: None,
            is_replicated: args.is_replicated,
        }
    }
}

impl From<crate::cdk::mgmt::HttpRequestResult> for HttpRequestResult {
    fn from(result: crate::cdk::mgmt::HttpRequestResult) -> Self {
        Self {
            status: result.status,
            headers: result.headers.into_iter().map(Into::into).collect(),
            body: result.body,
        }
    }
}