TL;DR: DFINITY is announcing flexible_http_request, a new management canister method in which a committee of nodes performs the same HTTPS request, and the canister receives their individual responses instead of one the subnet agreed on. This makes non-deterministic APIs usable without relying on complex transform functions. The new method lets a caller choose the cost, latency and trust trade-off per call, and reports failures as a typed error.
Background
Each subnet’s management canister offers the functionality to make HTTPS requests via two modes of the http_request method:
A fully replicated outcall has every node perform the request and requires a supermajority of them to agree on the response byte for byte. By reaching such an agreement, the result is trustworthy without having to trust any single node. However, it also means anything non-deterministic breaks the call. For instance, this could be a timestamp in the body, a per-request ID, a live price that ticks between requests, or the output of a language model. The usual workaround is a transform function that strips the varying parts, but this breaks cases in which the varying part is the one of interest.
A non-replicated outcall drops the agreement requirement entirely. It is the right tool for a non-idempotent request such as sending an email, but it does so by having one node perform the call, and that node has to be fully trusted.
flexible_http_request represents a compromise between the two. The caller chooses how many nodes perform the request and how many of their responses are needed. The individual answers reach the canister rather than being reduced to one. A call that cannot meet what was asked for returns a typed error, allowing different failure cases to be handled programmatically.
How it works
A flexible outcall is configured with three counts:
replication : opt record {
min_responses : nat32; // the fewest responses a successful outcall may carry
max_responses : nat32; // the most the caller is willing to receive
total_requests : nat32; // how many nodes issue the HTTP request
}
They must satisfy 0 <= min_responses <= max_responses <= total_requests and 1 <= total_requests <= N, where N is the number of nodes on the subnet, available from the new ic0.subnet_self_node_count system API. Omitting replication defaults to min_responses = floor(2 / 3 * N) + 1, max_responses = N and total_requests = N, which asks every node and implies an honest majority of responses.
min_responses determines when the call returns, and what its fault tolerance should be: setting it below total_requests means the outcall still succeeds when a committee node is down or slow. A successful result therefore carries between min_responses and max_responses responses, and how many is not known in advance.
Reconciling the responses is the canister’s job. Some approaches that fit different response shapes could be the following:
- Median, or a trimmed mean, over a numeric field. Tolerates a minority of outliers and stale feeds, which could be relevant for fetching price information from several sources.
- Majority vote over a normalized form of the response: hash the response after normalization, then take the most frequent hash. This is the fully replicated guarantee rebuilt in the canister, except that the canister chooses the replication.
- Agreement on a single field, ignoring differences elsewhere. Two price quotes can disagree on timestamp and request ID and still be a usable pair, i.e. as long as the price matches to two decimals.
- First usable response, when one live answer is enough but
min_responsesis greater than one to improve fault tolerance.
Two properties of the delivered set constrain those choices. The responses do not identify the node that produced them and their order is unspecified, so a strategy cannot weigh, prefer or exclude particular nodes. Additionally, the selection is size-biased: candidates are considered smallest-response-first, so when not all of them fit, it is the smallest responses that are delivered, which matters if response size correlates with content.
Each node runs the transform function on its own response, if one is set. It is no longer there to force agreement, but it is still the right place to drop headers or fields that are not needed, because response size drives most of the cost.
Setting min_responses = max_responses = 0 gives a fire-and-forget outcall: it returns an empty success as soon as any committee member responds, and therefore reserves no cycles for response delivery. In this case there is no guarantee whether any call actually succeeded or failed.
Note that PUT, DELETE and PATCH are accepted only when min_responses == max_responses == total_requests. This is to prevent unexpected race conditions between multiple outcalls made by the same canister. GET, HEAD and POST are unrestricted.
What comes back
The result is a variant, differentiating a success from potential error cases:
variant {
ok : vec http_request_result;
err : record {
global_error : opt variant { timeout; out_of_cycles; responses_too_large; too_many_rejects };
node_details : vec flexible_http_node_detail;
message : text;
};
}
A failed HTTPS outcall is still a successful call. When the requested replication cannot be met, the management canister replies with err instead of rejecting. The failure shows up in the returned value, not in the call’s own result. Error handling that only checks whether the call succeeded will miss it entirely. Only problems detected before the HTTP requests are dispatched come back as rejects: invalid arguments, replication counts that violate the constraints, or too few attached cycles to cover the base fee.
The four global errors:
timeout: fewer thanmin_responsessuccessful responses were collected within the system-defined timeout of one minute.too_many_rejects: more thantotal_requests - min_responsesnodes returned rejects, somin_responsessuccessful responses can no longer be collected. For instance, a node rejects when it cannot reach the server, when the response or the transform output exceeds the size limit it enforces, when the transform traps, when its own time limit expires, or when it exhausts its share of the attached cycles.responses_too_large: the smallest combination of responses that would have to be delivered together does not fit the block space available for outcall responses.out_of_cycles: what the nodes left unspent no longer covers delivering any result the call could still produce.
node_details gives per-node visibility, and which nodes appear depends on the error: timeout carries none, too_many_rejects lists the rejecting nodes it selected, and responses_too_large and out_of_cycles list every node whose response the system has seen. It is deliberately not a contract. It is not guaranteed to list every node. The diagnostic code strings are not a stable enumeration. The per-node resource report is not populated yet: every field is optional, and today all of them are absent. Use node_details to debug, not to drive control flow.
Limitations
The block limit applies to a combination of responses, not to each one. A block currently has 2 MiB (2_097_152 bytes) for HTTPS outcall responses. If the smallest responses that would have to be delivered together exceed this limit, a responses_too_large error is delivered instead. Note that one large response among many small ones is fine as long as the combination that has to be delivered fits. For that reason, a successful outcall may deliver fewer than max_responses: the selection shrinks until it fits the block limit.
Note that this is a different limit from the cap on a single response, which is max_response_bytes (default 2_000_000 bytes, decimal), enforced per node.
Canisters only. flexible_http_request cannot be called by ingress messages, only by canisters.
Pricing is version 2 only. Flexible outcalls are always priced with the new pay-as-you-go model. Two details are specific to flexible calls: the withheld budget is split between the total_requests nodes rather than across the whole subnet. A node that exhausts its own share of the budget produces a reject, which counts towards too_many_rejects.
How to use it
In Rust, the ic-cdk-management-canister crate exposes a builder that computes and attaches the cycles:
use ic_cdk_management_canister::{
FlexibleHttpRequest, FlexibleHttpRequestResult, ReplicationCounts,
};
// ask 3 nodes, accept any 2 or 3 answers
let result = FlexibleHttpRequest::new("https://example.com/price")
.with_replication(ReplicationCounts {
min_responses: 2,
max_responses: 3,
total_requests: 3,
})
.with_max_response_bytes(4_000)
.with_expected_roundtrip_time_ms(300)
.send()
.await?;
match result {
FlexibleHttpRequestResult::Ok(responses) => {
// between min_responses and max_responses of them
}
FlexibleHttpRequestResult::Err(err) => {
// branch on err.global_error; node_details is for diagnostics only
}
}
Documentation:
- HTTPS outcalls skill
flexible_http_requestin the interface specification- HTTPS outcalls concepts and guide
- Flexible outcalls example
- Pay-as-you-go pricing forum post
Future work
The per-node resource report is defined but not yet populated, so today a failed outcall reports which way it failed and which nodes were involved, but not necessarily what those nodes consumed. Filling it in is planned, which will make node_details useful for diagnosing a failure rather than just describing it.
The block space reserved for HTTPS outcalls is currently limited. This is because responses are carried in blocks in full. However, there is a known path to raising this limit substantially, which is replacing response content with hashes. Until then, the sum of responses must fit into a size limit of a single block.
Additionally, Motoko support for flexible outcalls is planned to arrive in a future release.