mcpField
H
https-outcalls

https-outcalls

github.com/dfinity开发39311 次安装今天更新
代码仓库

关于 https-outcalls

面向 Internet Computer Motoko 和 Rust canister 开发者的 HTTPS outcalls 指南,说明如何从链上代码请求外部 Web 服务。它按语言和依赖版本整理调用模式、响应转换、计费及常见失败原因。

主要功能

  1. 01调用外部 HTTPS 服务
  2. 02复制、非复制与 flexible 模式说明
  3. 03响应转换与共识注意事项
  4. 04按依赖版本区分调用方式与计费
  5. 05请求限制和错误处理指导

适用场景

  1. 01从 canister 获取公开 Web API 数据
  2. 02为复制请求规范化响应以达成共识
  3. 03为非幂等 POST 请求选择调用模式
  4. 04诊断 outcall 的大小限制或超时失败

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dfinity/icskills --skill https-outcalls

安装后在 Claude Code 中输入/https-outcalls确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
flexible-outcalls.md
json-responses.md
pricing-version-2.md

详细介绍

HTTPS 外部调用

这是什么

HTTPS 外部调用允许 canister 直接从链上代码向外部 Web 服务发送 HTTP 请求。在默认的复制模式下,子网上的每个节点都会发送相同的请求,并且必须就响应达成一致。转换函数会剔除非确定性字段(时间戳、请求 ID、排序),以便每个副本看到完全相同的响应并达成共识。

另外两种模式可以跳过这一一致性步骤:非复制(is_replicated = false,由一个节点发送请求)和灵活(flexible_http_request,由一个节点委员会分别返回各自的响应)。请参阅外部调用模式。

支持矩阵

RustMotoko
软件包ic-cdk 0.20.3+,搭配 ic-cdk-management-canister 0.2+ic 4.x(mops),需要 core 2.5.0+ 和 moc 1.4.0+
入口点HttpRequest::new(url) 构建器,.send()Call.httpRequest(args)
定价版本 1(旧版)只能通过固定使用 ic-cdk-management-canister 0.1,或直接调用 aaaaa-aa 并使用 ic_cdk::api::cost_http_request支持,这是 Call.httpRequest 使用的版本
定价版本 2(按需付费)支持,构建器始终会选择此版本—(不可用,需要尚未发布的 moc)
flexible_http_request支持,FlexibleHttpRequest::new(url)—(不可用,原因相同)
subnet_self_node_countic_cdk::api::subnet_self_node_count()—(不可用,原因相同)

编写代码前请先阅读本节;具体支持情况取决于项目所用的语言和依赖版本。目前 Motoko 代码一律使用版本 1。已发布的 ic 4.x HttpRequestArgs 没有 pricing_version 字段,Candid 会省略不存在的可选字段,副本会将其读取为版本 1——这正是 Call.httpRequest 预付费用的版本。无需采取变通措施;但目前不要指望 Motoko 能享受版本 2 的经济机制。

对于 Rust,正确的代码取决于项目属于以下三种情况中的哪一种。ic-cdk 0.19 提供 ic_cdk::management_canister::http_request(&args)(版本 1;该模块已在 0.20 中移除)。ic-cdk 0.20 搭配 ic-cdk-management-canister 0.1 时,提供 ic_cdk_management_canister::http_request(&args)(版本 1)。ic-cdk 0.20.3+ 搭配 ic-cdk-management-canister 0.2 时,提供构建器,并且仅支持版本 2:0.2.0 移除了自由函数 http_request,而构建器将 pricing_version: Some(2) 硬编码且不提供关闭选项,因此升级这个 crate 本身就是迁移。Rust 还需要 serde_json 来解析 JSON。

应该编写哪个版本的代码。 版本 1 已弃用,版本 2 才是发展方向,因此新的 Rust 代码应使用 0.2 构建器。但若项目要迁移只是某项无关任务的附带影响,不要因此提高项目锁定的版本:0.2 属于破坏性变更(会删除自由函数 http_request,而 HttpRequestArgs 会新增必填字段),是否升级应由调用方决定。请根据项目当前使用的版本编写正确代码,并告知对方可以升级。

Canister ID

HTTPS 外部调用使用 IC 管理 canister:

名称Canister ID用途
管理 canisteraaaaa-aahttp_request 和 flexible_http_request 管理调用的目标

无需额外部署任何内容。每个子网都内置了管理 canister。

会导致构建失败的错误

  1. 忘记提供转换函数。 在复制模式下,如果没有转换函数,各副本收到的原始 HTTP 响应通常会有所不同(例如,标头不同、JSON 字段顺序不同、时间戳不同)。共识会失败,调用也会被拒绝。此模式下务必提供转换函数。在非复制和灵活模式下,转换函数对于达成共识是可选的,因为每个节点自己的响应都会被传递。是否在这些模式下设置转换函数,需要权衡。不设置转换函数就不会为其预留费用(该指令项默认为 0,而不是查询上限),这是成本最低的配置。添加转换函数后,该指令项默认值会变为完整的查询上限,因此你还必须声明 with_expected_transform_instructions。这样做能省回传递费用;该费用按转换后的大小以字节计费:剔除的每个字节都能降低费用,因此关键在于可剔除内容的多少(臃肿的标头值得剔除,而响应主体本身已占大部分内容时则不然)。预留会将该指令项的下限设为约 1 KB,因此对于较小的响应,get_cost() 会低估节省的费用。此外,还有两个与周期无关的原因可能让你无论如何都要设置转换函数:在灵活模式下,合计 2 MiB 的预算是在转换后计算的,因此响应越小,被丢弃的响应就越少;如果每个节点的细微差异导致所有响应都不相同,就无法进行协调。

  2. 未为调用附加 cycles。 在普通的 Application 子网中,HTTPS 外部调用并非免费——调用方 canister 必须附加足够的 cycles 来支付费用;附加 0 会导致调用失败。Motoko 和 Rust 都提供了可自动计算并附加所需 cycles 的封装:在 Motoko 中,使用 ic mops 包中的 await Call.httpRequest(args)(import Call "mo:ic/Call");在 Rust 中,使用 ic-cdk-management-canister 0.2 中 HttpRequest 构建器的 .send()。使用哪个定价版本取决于封装:Motoko 的 Call.httpRequest 通过 ic0.cost_http_request(request_size, max_response_bytes) 使用定价版本 1,而 Rust 的 HttpRequest::send() 通过 ic0.cost_http_request_v2 使用定价版本 2。两者都能感知成本计划,因此同一封装在任何类型的子网上都会附加正确的数量。在版本 2 下,调用前的检查仅针对基础费用,因此附加的 cycles 不足时,调用时不会被拒绝:调用会在更严格的每节点限制下运行,并可能在中途失败,而此时远程服务已经收到请求。请参阅 references/pricing-version-2.md。在云引擎(CloudEngine 子网)上,该数量按设计始终为 0;不要在此处附加硬编码的非零费用来“修复”原本正常的外部调用——请参阅 cloud-engine-canisters 技能。

  3. 使用 HTTP 而不是 HTTPS。 IC 仅支持 HTTPS 外部调用。纯 HTTP URL 会被拒绝。目标服务必须具有有效的 TLS 证书。

  4. 根据预期的响应主体设置 max_response_bytes——上限不只针对主体。 规范将 HTTP 请求或响应的大小定义为表示 HTTP 标头名称和值以及 HTTP 主体的字节总数。响应标头也计入 max_response_bytes。在收到主体的第一个字节之前,实际 API 通常会先发送 1–2 KB 的响应标头(唯一的请求 ID、Date、速率限制相关标头、CDN 标头);对于较严格的上限,这会占用预算的很大一部分。应根据从服务端接收的标头 + 主体设置上限,并留出余量。此问题会导致彻底失败:调用每次都会失败,而不是返回截断后的响应。最大值为 2MB = 2_000_000 字节(十进制,不是 2^21);同样按照标头加主体的定义,你发送的请求上限也是 2_000_000 字节。

  5. 期望转换函数能将超出上限的响应缩小到上限以内。 上限会检查两次:一次是在原始响应从服务端到达时(先检查标头,再根据剩余空间检查正文),另一次是在转换函数的 Candid 编码输出上,其中包含序列化开销。在转换函数中剥离标头,无法挽救已经超出上限的原始响应,因为第一次检查会在转换函数运行之前失败。这么做可以让转换函数自身的输出不超出上限——如果转换函数原本会将标头也回传,导致编码后的输出超限,那么剥离标头就值得一试。两次检查使用的都是同一个 max_response_bytes 值,这就是为什么刚好满足上限的原始响应,在转换后仍可能失败:Candid 开销会叠加在原始响应之上。因此,max_response_bytes 应按原始响应的大小设置,并确保转换函数的输出较小;切勿抱着「之后剥离标头就能让超限响应符合要求」的想法设置过于严格的上限。

  6. 忽略标头限制。 无论 max_response_bytes 如何设置,规范都将 HTTP 请求和响应限制为最多 64 个标头,每个标头名称或值最多 8 KiB,所有标头名称和值合计最多 48 KiB。URL 长度不得超过 8192 字节。在请求端,当副本解码你的参数时会执行这些限制,因此超限请求不会离开子网,而是以 InvalidManagementPayload 失败——例如 Deserialize error: The number of elements exceeds maximum allowed 64——而不是返回任何类似 HTTP 的错误。如果你没有发送 user-agent 标头,IC 会添加 user-agent: ic/1.0,且该添加的标头不计入这些限制。

  7. 省略 max_response_bytes。 在版本 1 下,如果不设置此值,系统会采用最大值(2MB)并据此收取 cycles——在 13 节点子网上约为 20.85 billion cycles。在版本 2 下,该值不再决定价格,但仍会作为 raw_response_bytes 预期值的默认值,因此省略此值会按 2MB 预留:34.0 billion cycles,而设置 4,000 字节上限时只需 5.3 billion cycles。无论哪种情况,都应始终将其设置为符合预期响应大小的合理上限(关于哪些内容计入该值,请参阅陷阱 4)。

  8. 未经谨慎处理就发送非幂等 POST 请求。 由于多个副本会发送相同的请求,非幂等的 POST 端点(例如「创建订单」)会被调用 N 次(每个副本一次,在 13 节点子网上通常为 13 次)。请使用幂等键,或设计端点以处理重复请求;也可以设置 is_replicated = ?false(Rust:.non_replicated()),由单个节点发送请求,从而完全消除速率限制带来的压力——代价是你必须信任该节点不会窥探或修改响应。

  9. 未处理外呼失败。 外部服务可能宕机、响应缓慢或返回错误。务必处理错误情况。这里有两种不同的超时,都不会触发陷阱——两者都会以拒绝形式返回(在 Motoko 中,await 会抛出可捕获的 Error;在 Rust 中,包装器会返回 Err):

    • 远程服务在 30 秒内没有响应:SysFatal,消息为 Timeout expired。
    • 子网在 60 秒内没有生成响应:SysTransient,消息为 Canister http request timed out。通常只有这种情况可以重试;例外是资金不足且缺少某个节点报告的版本 2 调用(参阅 references/pricing-version-2.md)。
  10. 调用 localhost 或私有 IP。 HTTPS 外呼只能访问公共互联网端点。localhost、10.x.x.x、192.168.x.x 和其他私有网段均会被阻止。

  11. 忘记设置 Host 标头。 有些 API 端点要求显式设置 Host 标头。IC 不会根据 URL 自动设置该标头。

  12. 不设置版本 2 的预期值。 未声明的任何项目都会按其最大值预留:60 秒往返时间,以及运行到完整查询指令上限的转换。对于 4,000 字节的上限,这意味着占用约 53 亿个周期,其中几乎全部是转换预留;而声明 with_expected_transform_instructions 和 with_expected_roundtrip_time_ms 后,约为 1.12 亿个周期。请声明这两项。raw_response_bytes 保持不变,因为它由服务端决定;如果字节数相关的费用占主导,则调低 max_response_bytes。当转换会限制自身输出时,请声明 with_expected_transformed_response_bytes;这是唯一一种无法通过调整上限来解决的情况。除非较小的预算确实会提前终止调用,否则这一切都不会影响费用。references/pricing-version-2.md 列出了具体数值、逐项资源说明以及故障模式。

  13. 指望通过 ic-cdk-management-canister 0.2 使用版本 1。 这行不通。该 crate 只提供 builder 这一种途径,并将 pricing_version: Some(2) 硬编码其中,也没有 with_pricing_version。HttpRequest::from_args 看似是个绕行办法,但其实不是:它是文档中迁移现有调用点的方式,而且会静默地将 pricing_version 覆盖为 2,因此,将有意设为 1 的参数传入后,版本就会改变。若要继续使用版本 1,请固定使用 ic-cdk-management-canister 0.1,或直接调用 aaaaa-aa,并使用仍存在于 0.20.3 中的 ic_cdk::api::cost_http_request 计算费用。

  14. 手动设置 pricing_version,却用错误的费用函数为其提供资金。 字段与附加的费用必须一致。若设置 pricing_version = 2,却附加 cost_http_request(版本 1)的金额,则调用会被接受,因为前置检查只验证基础费用;随后调用会获得低于版本 2 预期的每节点预算,可能在中途失败。反过来,用 cost_http_request_v2 的金额为版本 1 调用提供资金,则会被直接拒绝,并显示 http_request request sent with <X> cycles, but <Y> cycles are required.,因为版本 1 要求预先提供完整的 max_response_bytes 费用。交由封装层同时设置两项:Rust 的 HttpRequest::send() 会将版本 2 与 cost_http_request_v2 配对,Motoko 的 Call.httpRequest 会将版本 1 与 cost_http_request 配对。另请注意,副本会将无法识别的版本过滤为版本 1,且不会报错,因此错误值会导致资金不匹配,而不是验证错误。

  15. 为灵活的外部调用硬编码 total_requests。 total_requests 不得超过子网大小,而子网大小因子网而异(主网上为 13 或 34)。请动态计算:

    // 错误:即使只需要五个节点也不行,因为 5 可能超过子网大小。
    let replication = ReplicationCounts { total_requests: 5, min_responses: 3, max_responses: 5 };
    
    // 正确:请求五个,但最多不超过子网拥有的节点数。
    let total_requests = subnet_self_node_count().min(5);
    let min_responses = total_requests / 2 + 1;
    

    在成功分支中,也要处理 min_responses 到 max_responses 之间的任何数量——少于 max_responses 也是正常成功,并非降级;还要分别检查每个响应的 status,因为各节点之间不必达成一致。请参阅 references/flexible-outcalls.md。

Outcall 模式

通过 is_replicated 选择复制或非复制模式;灵活模式则使用单独的方法。

复制(默认)非复制(is_replicated = false)灵活(flexible_http_request)
由谁发送所有 N 个节点系统选出的一个节点由 total_requests 个节点组成的委员会
可获得的结果一致通过的响应该节点的响应min_responses 至 max_responses 个响应
转换实际上必需可选,但通常仍值得执行可选,但通常仍值得执行
定价版本 1 或 2版本 1 或 2始终为版本 2
额外方法GET、HEAD、POST另外支持 PUT、DELETE、PATCH当 total_requests、min_responses 和 max_responses 全部相等时,也支持这些方法
风险N 个并发请求可能触发 API 速率限制该节点可能观察或修改响应你需要负责协调这些响应

需要共识提供的完整性保证时,使用复制模式;对于有速率限制的 API 和非幂等 POST 请求,使用非复制模式;如果数据变化速度快到节点根本无法达成一致,则使用灵活模式。

实现

Motoko

从 ic mops 包中同时导入封装和类型:import Call "mo:ic/Call" 和 import IC "mo:ic/Types"。Call.httpRequest 会计算并附加所需的 cycles。

这是定价版本 1。 Motoko 尚无版本 2 路径,也不支持灵活的外部调用(请参阅支持矩阵)。以下内容均正确且受支持,只是采用了旧版定价模型。

import Blob "mo:core/Blob";
import Nat "mo:core/Nat";
import Text "mo:core/Text";
import Call "mo:ic/Call";
import IC "mo:ic/Types";

persistent actor {

  // Transform function: strips headers so all replicas see the same response for consensus.
  // MUST be a `shared query` function.
  public query func transform({
    context : Blob;
    response : IC.HttpRequestResult;
  }) : async IC.HttpRequestResult {
    {
      response with headers = []; // Strip headers -- they often contain non-deterministic values
    };
  };

  // GET request: fetch a JSON API
  public func getIcpPriceUsd() : async Text {
    let url = "https://api.coingecko.com/api/v3/simple/price?ids=internet-computer&vs_currencies=usd";

    let request : IC.HttpRequestArgs = {
      url = url;
      // Always set — omitting defaults to 2MB and charges accordingly.
      // Budget for response headers + body: the cap covers both, and the
      // transform cannot bring an oversized response back under it.
      max_response_bytes = ?(10_000 : Nat64);
      headers = [
        { name = "User-Agent"; value = "ic-canister" },
      ];
      body = null;
      method = #get;
      transform = ?{
        function = transform;
        context = Blob.fromArray([]);
      };
      is_replicated = null;
    };

    // Call.httpRequest computes and attaches the required cycles automatically
    let response = await Call.httpRequest(request);

    switch (Text.decodeUtf8(response.body)) {
      case (?text) { text };
      case (null) { "Response is not valid UTF-8" };
    };
  };

  // POST transform: also discards the body, because httpbin.org echoes the
  // sender's IP in "origin", which differs across replicas.
  public query func transformPost({
    context : Blob;
    response : IC.HttpRequestResult;
  }) : async IC.HttpRequestResult {
    {
      response with
      headers = [];
      body = Blob.fromArray([]);
    };
  };

  // POST request: send JSON data
  public func postData(jsonPayload : Text) : async Text {
    let url = "https://httpbin.org/post";

    let request : IC.HttpRequestArgs = {
      url = url;
      max_response_bytes = ?(50_000 : Nat64);
      headers = [
        { name = "Content-Type"; value = "application/json" },
        { name = "User-Agent"; value = "ic-canister" },
        // Idempotency key: prevents duplicate processing if multiple replicas hit the endpoint
        { name = "Idempotency-Key"; value = "unique-request-id-12345" },
      ];
      body = ?Text.encodeUtf8(jsonPayload);
      method = #post;
      transform = ?{
        function = transformPost;
        context = Blob.fromArray([]);
      };
      is_replicated = null;
    };

    // Call.httpRequest computes and attaches the required cycles automatically
    let response = await Call.httpRequest(request);

    if (response.status == 200) {
      "POST successful (status 200)";
    } else {
      "POST failed with status " # Nat.toText(response.status);
    };
  };
};
Rust
# Cargo.toml
[package]
name = "https_outcalls_backend"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib"]

[dependencies]
ic-cdk = "0.20"
ic-cdk-management-canister = "0.2"
candid = "0.10"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
use ic_cdk_management_canister::{
    transform_context_from_query, HttpMethod, HttpRequest, HttpRequestResult, TransformArgs,
};
use ic_cdk::{query, update};

/// Transform function: strips non-deterministic headers so all replicas agree.
/// MUST be a #[query] function.
#[query(hidden = true)]
fn transform(args: TransformArgs) -> HttpRequestResult {
    HttpRequestResult {
        status: args.response.status,
        body: args.response.body,
        headers: vec![], // Strip all headers for consensus
        // If you need specific headers, filter them here:
        // headers: args.response.headers.into_iter()
        //     .filter(|h| h.name.to_lowercase() == "content-type")
        //     .collect(),
    }
}

/// GET request: Fetch JSON from an external API
#[update]
async fn fetch_price() -> String {
    let url = "https://api.coingecko.com/api/v3/simple/price?ids=internet-computer&vs_currencies=usd";

    // The builder always selects pricing version 2. `.send()` prices the call
    // with ic0.cost_http_request_v2 and attaches that amount.
    let request = HttpRequest::new(url)
        .with_method(HttpMethod::GET)
        // Budget for response headers + body: the cap covers both, and the
        // transform cannot bring an oversized response back under it.
        .with_max_response_bytes(10_000)
        .with_header("User-Agent", "ic-canister")
        .with_transform(transform_context_from_query("transform".to_string(), vec![]))
        // Unset expectations reserve the worst case: a 60s round trip and a
        // transform running to the full 5-billion-instruction query limit.
        // Declaring them holds far fewer cycles. See pitfall 12.
        .with_expected_roundtrip_time_ms(10_000)
        .with_expected_transform_instructions(1_000_000);

    match request.send().await {
        Ok(response) => {
            let body = String::from_utf8(response.body)
                .unwrap_or_else(|_| "Invalid UTF-8 in response".to_string());

            if response.status != candid::Nat::from(200u64) {
                return format!("HTTP error: status {}", response.status);
            }

            body
        }
        Err(err) => {
            format!("HTTP outcall failed: {:?}", err)
        }
    }
}

/// POST transform: strips headers AND body, because httpbin.org echoes the
/// sender's IP in "origin", which differs across replicas.
#[query(hidden = true)]
fn transform_post(args: TransformArgs) -> HttpRequestResult {
    HttpRequestResult {
        status: args.response.status,
        body: vec![],
        headers: vec![],
    }
}

/// POST request: Send JSON data to an external API
#[update]
async fn post_data(json_payload: String) -> String {
    let url = "https://httpbin.org/post";

    let request = HttpRequest::new(url)
        .with_method(HttpMethod::POST)
        .with_max_response_bytes(50_000)
        .with_header("Content-Type", "application/json")
        .with_header("User-Agent", "ic-canister")
        // Idempotency key: prevents duplicate processing across replicas
        .with_header("Idempotency-Key", "unique-request-id-12345")
        .with_body(json_payload.into_bytes())
        .with_transform(transform_context_from_query("transform_post".to_string(), vec![]))
        .with_expected_roundtrip_time_ms(10_000)
        .with_expected_transform_instructions(1_000_000);
    // Replicated, so every node POSTs: hence the idempotency key above, and
    // `transform_post`. `.non_replicated()` would send it once instead and make
    // both unnecessary, at the cost of trusting that one node (pitfall 8).

    match request.send().await {
        Ok(response) => {
            if response.status == candid::Nat::from(200u64) {
                "POST successful (status 200)".to_string()
            } else {
                format!("POST failed with status {}", response.status)
            }
        }
        Err(err) => {
            format!("HTTP outcall failed: {:?}", err)
        }
    }
}
周期成本估算

有两个定价版本。版本 1 是副本默认版本,现已弃用;版本 2(“按使用量付费”)按调用消耗的资源收费,而不是按预留的字节数收费。**Motoko 仅支持版本 1;使用 ic-cdk-management-canister 0.2 的 Rust 仅支持版本 2。**以下介绍的版本 1 仍是副本默认版本;如需了解完整的版本 2,包括预留机制及其独有的故障模式,请阅读 references/pricing-version-2.md。

ic0.cost_http_request 系统 API 会在运行时计算确切的周期成本,因此 Canister 无需硬编码公式。ic mops 包中的 Call.httpRequest(Motoko)会在内部调用该 API,并自动附加所需周期;Rust 端的 ic-cdk-management-canister 0.1 中的免费 http_request,以及 ic-cdk 0.19 中的 ic_cdk::management_canister::http_request 也有同样行为。如需手动调用:Motoko 使用 Prim.costHttpRequest(requestSize, maxResponseBytes)(通过 import Prim "mo:⛔");Rust 使用 ic_cdk::api::cost_http_request(request_size, max_res_bytes)。

request_size 是 URL、所有请求头名称和值、请求正文、转换函数名称以及转换上下文的字节长度之和。

作为参考,13 节点子网(n = 13)上的底层公式为:

Base cost:                      49_140_000 cycles  (= (3_000_000 + 60_000*13) * 13)
+ per request byte:              5_200 cycles      (= 400 * 13)
+ per max_response_bytes byte:  10_400 cycles      (= 800 * 13)

IMPORTANT: Under version 1 the charge is against max_response_bytes, NOT the
actual response size. Under version 2 it is the reverse: the charge follows
the bytes that arrive, the round-trip time and the transform instructions.
Omitting max_response_bytes assumes the 2MB maximum (2_000_000 bytes) and costs
49_140_000 + 10_400 * 2_000_000 = 20_849_140_000 cycles (~20.85B), plus the
per-request-byte term.

在版本 1 下,调用返回时会退还未使用的周期,因此超额预算安全,但并非免费。在版本 2 下,超额附加周期不只是浪费:超过基础费用的部分是每个节点的额度,它会设定每个节点的响应期限和转换指令限制,因此,预留余量相当于授予消费权限,而不是留作闲置储备。版本 2 的退款也会异步到账;如果某个节点始终未报告,则会在响应返回约一分钟后退还其额度。附加的周期在调用持续期间会从 Canister 的可用余额中扣除,因此,手动附加的余量会直接限制 Canister 在余额耗尽前可同时发起的出站调用数量。对于每次用户操作都发起一次出站调用的 Canister 来说,该余量就是并发限制。这就是为什么两个封装都会附加精确计算出的金额,而不是一个整数:正如 ic mops 包所说:“调用只会添加最少量的周期。这有助于 Canister 在周期耗尽前并行发起更多调用。”

不要为了“保险”而手动附加缓冲额度。请调用封装,或使用 Prim.costHttpRequest / ic_cdk::api::cost_http_request 计算确切成本,并附加该金额。在版本 1 下,降低出站调用成本的方法是收紧 max_response_bytes,而不是增加附加金额。在版本 2 下,价格取决于调用的实际消耗;往返时间和转换由 with_expected_* 设置项控制,大小仍由 max_response_bytes 控制,但它现在决定的是预留额度,而非价格。

此公式适用于普通的Application 子网。它不适用于云引擎(CloudEngine 子网):在云引擎上,无论 request_size 或 max_response_bytes 为何,ic0.cost_http_request 都会返回 0,因为引擎采用免费的成本方案。在编写或调试将在云引擎上运行的出站调用代码前,请加载 cloud-engine-canisters 技能,了解引擎的调用规则。

部署与测试

本地部署
icp network start -d
icp deploy backend

注意:HTTPS 出站调用可在本地副本上运行。icp-cli 会通过本地 HTTP 网关代理请求。灵活出站调用也可在本地运行,但所有响应都来自同一个节点,因此协调过程不会在此处发现意见分歧。

主网部署
icp canister status backend -e ic && icp deploy -e ic backend   # check balance, then deploy

验证是否正常运行

# 1. GET outcall. Expected: '("{\"internet-computer\":{\"usd\":12.34}}")', price varies.
icp canister call backend getIcpPriceUsd '()'   # Motoko example above
icp canister call backend fetch_price '()'      # Rust example above

# 2. POST outcall. Expected: httpbin.org echoing your data back.
icp canister call backend postData '("{\"test\": \"hello\"}")'    # Motoko
icp canister call backend post_data '("{\"test\": \"hello\"}")'   # Rust

# 4. Balance should have decreased. Under version 2 it keeps settling for about
#    a minute afterwards as per-node refunds arrive, so re-read before judging.
icp canister status backend

# 5. Error handling: add a function calling a non-existent domain and verify it
#    returns an error rather than trapping.
出站调用故障排查

如果出站调用失败:

# Check the replica log for detailed error messages
# Local: icp output shows errors inline
# Mainnet: check the canister logs

# Exact reject messages from the replica (match on these, not on paraphrases):
#
# "Timeout expired"                                                     [SysFatal]
#     The remote server did not respond within 30s.
#
# "Canister http request timed out"                                  [SysTransient]
#     The subnet did not produce a response within 60s. Normally retryable.
#     Under version 2 it can also mean under-funding, but only when a node went
#     silent AND the remaining contributors cannot fund delivery without it;
#     then a retry just times out again. See references/pricing-version-2.md.
#
# "Deadline Exceeded"                                                [SysTransient]
#     The adapter did not answer the replica within its 60s deadline. Rarer
#     than the two above: the adapter's own 30s timeout usually fires first.
#
# "No consensus could be reached. Replicas had different responses.
#  Details: request_id: <id>, hashes: <...>"                         [SysTransient]
#     Transform is missing or not stripping enough non-determinism.
#
# There are THREE distinct size-limit messages, all [SysFatal], because response
# headers are counted against max_response_bytes BEFORE the body is read:
#
# "Header size exceeds specified response size limit <N>"
#     The response headers ALONE exceeded max_response_bytes.
#
# "Http body exceeds size limit of <N> bytes."
#     The body exceeded the allowance REMAINING after header bytes were
#     subtracted. Note the message prints the full cap <N>, not the remainder,
#     so the body that failed can be well under <N>. Do not read this message
#     as "my body is too big" -- it means headers + body are too big.
#
# "Transformed http response exceeds limit: <N>"
#     The Candid-encoded output of your transform exceeded max_response_bytes.
#
#     Raising max_response_bytes fixes all three. Stripping headers in the
#     transform fixes ONLY this last one: the first two are checked before
#     the transform runs.
#
# "http_request request sent with <X> cycles, but <Y> cycles are required."
#                                                              [CanisterReject]
#     Attached less than the computed cost. Use the wrapper rather than a
#     hand-picked number. Version 1 only: version 2 checks just the base fee up
#     front and fails later instead, as one of the next two.
#
# "Insufficient cycles"                                        [CanisterReject]
#     Version 2: a node exhausted its own per-node allowance. Ordinary response
#     content, so it reaches the canister only if a quorum produce the same
#     reject; otherwise it becomes a divergence.
#
# "Out of cycles: <k> of the assigned replicas reported a collective spend of
#  <n> cycles, leaving <m> cycles of the attached payment (after base fee
#  deduction). Delivering a response would cost at least <min> cycles."
#                                                              [CanisterReject]
#     Version 2 pooled shortfall: the nodes agreed, but what they left unspent
#     does not cover putting the response in a block. Raise the reservation.
转换调试

如果遇到“no consensus could be reached”错误,说明你的转换函数未能让响应保持一致。常见原因:

  1. 响应标头不同——在转换中剥离所有标头
  2. JSON 字段顺序不同——在转换中解析并重新序列化 JSON
  3. 响应正文中包含时间戳——仅提取所需字段

如需了解类型化 JSON 解析器和用于规范化字段顺序的转换,请参阅 references/json-responses.md。

其他参考资料

  • references/pricing-version-2.md — 按需付费模式:收费方式、四项预期及其默认值、为何将其纳入每节点的共享预算,以及资金不足时的调用拒绝消息和迁移前的检查事项。
  • references/json-responses.md — 使用 serde_json 解析类型化 JSON 正文,以及通过重新序列化来规范化字段顺序以达成共识的转换。
  • references/flexible-outcalls.md — flexible_http_request 完整说明:根据 subnet_self_node_count 计算复制数量、可编译示例、协调策略及其威胁模型、四种 global_error 值、两个独立的大小限制、本地运行为何始终不会显示意见不一致,以及转换是否值得投入。
  • 对于在云引擎上运行的 canister,请加载 cloud-engine-canisters,其中包括为何出站调用的成本会降至 0,以及为何绝不能通过引擎的控制台代理路由出站调用。

评论

支持 Markdown