Skip to content

fix(https-outcalls): correct max_response_bytes semantics and the 2MB default cost - #352

Merged
marc0olo merged 5 commits into
mainfrom
fix/https-outcalls-max-response-bytes
Aug 25, 2026
Merged

fix(https-outcalls): correct max_response_bytes semantics and the 2MB default cost#352
marc0olo merged 5 commits into
mainfrom
fix/https-outcalls-max-response-bytes

Conversation

@marc0olo

@marc0olo marc0olo commented Aug 25, 2026

Copy link
Copy Markdown
Member

Closes #351.

The issue reported two problems with how the HTTPS outcalls pages describe max_response_bytes. Both are confirmed against the interface spec and fixed here, along with several further defects found while fixing them.

What the issue reported

1. Wrong byte figure. 2,097,1522,000,000. The spec: "the default value of 2MB (2,000,000B) is used as the limit." Confirmed in the replica as MAX_CANISTER_HTTP_RESPONSE_BYTES = 2_000_000.

2. The limit is not body-scoped. The spec defines the measured quantity as "the total number of bytes representing the names and values of HTTP headers and the HTTP body." Both pages now say headers plus body.

3. The transform bound (raised in the issue body). max_response_bytes is enforced twice: on the raw response as it arrives, and again on the transform's output. A transform cannot rescue a response that already exceeded the cap, because the first check runs before the transform does; it only keeps the transform's own output within the cap. Stated in the guide's transform section, where a reader would form the "I'll strip headers to fit" plan, and in the concepts Limitations bullet.

Additional defects found

4. The default-size cost was wrong on both pages. Both said omitting max_response_bytes costs ~21.5 billion cycles. The formula already published on references/cycle-costs.md gives:

49_140_000 + 10_400 * 2_000_000 = 20_849_140_000   (~20.85 billion)

Corrected to ~20.85 billion in both places. 21.5B matches neither the decimal nor the binary reading, so it appears independently wrong rather than downstream of the byte-figure error.

5. references/cycle-costs.md said max_response_bytes defaults to "2 MiB". Same decimal-vs-binary error, on the page the other two link to for exact pricing. Corrected, with the resulting cycle figure added.

6. Both pages claimed a single ~30 second timeout, and the guide said the call traps. There are two timeouts and neither traps:

Trigger Reject Message
Remote server silent for 30s SysFatal Timeout expired
Subnet produces no response within 60s SysTransient Canister http request timed out

Telling readers to expect a trap points them at the wrong error handling.

7. The Motoko cycle guidance was stale. Both pages said "In Motoko, cycles must be attached explicitly with await (with cycles = ...)". The ic package provides Call.httpRequest, which computes the exact cost via ic0.cost_http_request and attaches it, matching the Rust wrapper. The pages now also explain why a hand-picked margin is counterproductive: attached cycles are held for the duration of the call, so a margin caps outcall concurrency.

Submodule bump

Item 7 could not be fixed in prose alone, because the embedded Motoko snippets hardcoded with cycles = 230_949_972_000: correcting the text would have left the page contradicting its own code. That was fixed upstream first in dfinity/examples#1477, merged as b4fe175.

.sources/examples is bumped d4ea422b4fe175 here, so the snippets now render await Call.httpRequest(request) and code and prose agree.

The old pin predated the examples restructure, so all six snippet= paths moved and are updated:

send_http_{get,post}/src/send_http_{get,post}_backend/main.mo    -> send_http_{get,post}/backend/main.mo
send_http_{get,post}/src/send_http_{get,post}_backend/src/lib.rs -> send_http_{get,post}/backend/src/lib.rs

Region names (transform, get_request, post_request) are unchanged.

Per .agents/submodule-bumping.md: guides/backends/https-outcalls.mdx is the only page using CodeExample, so no other page is affected by the moves, and examples tracks master so it carries no .sources/VERSIONS entry.

Scope

Kept deliberately tight per CONTRIBUTING.md: concepts/ stays explanatory, and the spec's header limits (≤64 headers, ≤8 KiB per name or value, ≤48 KiB total) are not added. The issue marked them optional, and enumerating them duplicates content that belongs in the interface spec and the https-outcalls skill.

Verification

  • npm run validate: no errors in the touched files.
  • build_and_deploy: passing against the new submodule. This is the meaningful check for the bump, since remark-snippet treats a missing file or region as a hard build error.
  • Before pushing the bump, all six file+region pairs were confirmed to resolve at b4fe175 by replicating the plugin's extraction logic.

Related

… default cost

The 2MB response limit was documented as 2,097,152 bytes and scoped to the
response body. Per the interface spec it is 2,000,000 bytes (decimal), it is
measured over header names and values plus the body, and it also bounds the
transform function's output.

Also corrects the cost of omitting max_response_bytes: ~20.85 billion cycles
on a 13-node subnet, not ~21.5 billion. The formula already on the cycle-costs
page gives 49_140_000 + 10_400 * 2_000_000 = 20_849_140_000.

- concepts/https-outcalls.md: byte figure, headers-plus-body scoping, transform
  bound, default-size cost
- guides/backends/https-outcalls.mdx: same, plus a note in the transform
  section that a transform cannot shrink a response under the cap
- references/cycle-costs.md: max_response_bytes defaults to 2 MB, not 2 MiB

Closes #351
@marc0olo
marc0olo requested a review from a team as a code owner August 25, 2026 10:14
@marc0olo
marc0olo marked this pull request as draft August 25, 2026 10:21
Both places claiming "In Motoko, cycles must be attached explicitly with
`await (with cycles = ...)`" are stale. The `ic` package provides
`Call.httpRequest`, which computes the exact cost via `ic0.cost_http_request`
and attaches it, matching the Rust wrapper. concepts/https-outcalls.md already
described both wrappers correctly.

Also notes why a hand-picked margin is counterproductive: attached cycles are
held for the duration of the call, so a margin caps outcall concurrency.

Depends on dfinity/examples#1477, which switches the embedded Motoko examples
off the hardcoded `with cycles = 230_949_972_000`. Requires a submodule bump
before merge.
@marc0olo

Copy link
Copy Markdown
Member Author

Converted to draft: this now depends on dfinity/examples#1477 and needs a submodule bump before it can merge.

Why

The guide stated in two places that "In Motoko, cycles must be attached explicitly with await (with cycles = ...)". That is stale (the ic package provides Call.httpRequest, and concepts/https-outcalls.md already described both wrappers correctly), but it matched the embedded example, which hardcoded with cycles = 230_949_972_000. Fixing the prose alone would have left the page contradicting its own snippet, so the example is fixed upstream first.

dfinity/examples#1477 switches both Motoko outcall examples to Call.httpRequest. The prose fix is now pushed here (70aec2a).

Blocking checklist

The snippet paths are not a no-op

Worth flagging for whoever does the bump. .sources/examples is pinned at d4ea422, which predates the examples restructure, so the bump moves these files regardless of #1477:

Current snippet= path After bump
send_http_get/src/send_http_get_backend/main.mo send_http_get/backend/main.mo
send_http_get/src/send_http_get_backend/src/lib.rs send_http_get/backend/src/lib.rs
send_http_post/src/send_http_post_backend/main.mo send_http_post/backend/main.mo
send_http_post/src/send_http_post_backend/src/lib.rs send_http_post/backend/src/lib.rs

Per .agents/submodule-bumping.md, examples bumps require verifying linked files still exist at the same path. These do not. The region names (transform, get_request, post_request) are unchanged and still correctly paired in #1477, so only the file paths need editing.

The factual corrections in the first commit (8b840b3) are independent of all of this and could be split out if the blocking is inconvenient.

Comment thread docs/guides/backends/https-outcalls.mdx Outdated
…t claim

Review feedback from @eichhorl, mirroring the corrections on dfinity/icskills#361.

The transform claim was too absolute. max_response_bytes is enforced twice:
on the raw response as it arrives, and again on the transform's own output.
Stripping headers in the transform cannot rescue a response that already
exceeded the cap, since that check runs first, but it does keep the
transform's own output within the cap.

Also corrects the timeout pitfall, which claimed the call traps. There are
two timeouts and neither traps: the remote server going silent for 30s
rejects with SysFatal, and the subnet failing to produce a response within
60s rejects with SysTransient.
@marc0olo

Copy link
Copy Markdown
Member Author

Thanks @eichhorl. Applied in e27a82b, mirroring the corrections on dfinity/icskills#361.

Transform nuance. The paragraph now says max_response_bytes is enforced twice, on the raw response and again on the transform's own output, so stripping headers cannot rescue a response that already exceeded the cap but does keep the transform's output within it.

One more found while applying it. The Limitations section claimed "If the external server does not respond within the timeout, the call traps." It does not trap, and there are two timeouts, not one: 30s for the remote server (SysFatal) and 60s for the subnet to produce a response (SysTransient). Corrected, since telling readers to expect a trap sends them toward the wrong error handling.

Still draft and still blocked on the submodule bump described above; this only addresses the review.

…e too

The review comment on the guide ("comments of dfinity/icskills#361 also apply
here") applies to concepts/https-outcalls.md as well, which this branch also
edits. Two bullets there still carried the claims the review corrected:

- The 2MB bullet said a transform "cannot bring an oversized response back
  under the cap" full stop. The cap is enforced twice against the same value:
  on the raw response in the adapter (rpc_server.rs:402-417, before the
  transform runs) and on the transform's Candid-encoded output
  (client.rs:250). Stripping headers cannot rescue a response that failed the
  first check, but it does keep the transform's own output under the second.

- The timeout bullet described a single ~30s timeout. There are two: 30s for
  the remote server (SysFatal, "Timeout expired") and 60s for the subnet to
  produce a response (SysTransient, "Canister http request timed out").

Both now match the wording already applied to guides/backends/https-outcalls.mdx.
Verified against dfinity/ic@339d220a83.
@marc0olo

Copy link
Copy Markdown
Member Author

Thanks @eichhorl, you were right that this reaches past the guide. concepts/https-outcalls.md, which this branch also edits, still carried both claims. Fixed in 7e10ca3.

  • 2 MB bullet. It said a transform "cannot bring an oversized response back under the cap", full stop. It now states the two checks against the same value: the raw response in the adapter (rpc_server.rs:402-417, which runs before the transform) and the transform's Candid-encoded output (client.rs:250). Stripping headers cannot rescue a response that failed the first check, but it does keep the transform's own output under the second.
  • Timeout bullet. It described a single ~30s timeout. Now both, matching the wording already on the guide.

Both pages read the same way now. Verified against dfinity/ic@339d220a83.

Still a draft, still blocked on the submodule bump described above.

marc0olo added a commit to dfinity/icskills that referenced this pull request Aug 25, 2026
…-budgeting, and reject messages (#361)

Closes #360.

All three points in the issue are valid. Verified independently against
the interface spec
(`developer-docs/docs/references/ic-interface-spec/management-canister.md`),
the `ic` mops package (`v4.2.0`), and the replica source
(`dfinity/ic@339d220a83`) rather than taking the issue at its word.

## 1. `max_response_bytes` is not body-only (pitfalls 4, 5, 6)

The skill said *"The maximum response **body** is 2MB"*. The spec
measures the limit over **header names and values plus the body**, and
the same definition caps the request you send. A real API commonly sends
1–2 KB of response headers before a single byte of body, which against a
tight cap is a large share of the budget, and the failure is total
rather than a truncation.

The spec also binds the **transform's output** to `max_response_bytes`
(including Candid serialization overhead), so a tight cap cannot be
rescued by stripping headers in the transform. That is now its own
pitfall, since it is the natural wrong inference from the old wording.

Also adds the spec's header limits (≤64 headers, ≤8 KiB per name or
value, ≤48 KiB combined, URL ≤8192) with the non-obvious part: on the
request side these are enforced when the replica **decodes your
arguments**, so an over-limit request never leaves the subnet and fails
with `InvalidManagementPayload`, not with anything HTTP-looking.

## 2. Over-budgeting cycles is safe but not free (Cycle Cost Estimation)

*"Unused cycles are refunded, so it is safe to over-budget"* was true
but misleading. Attached cycles leave the canister's spendable balance
for the **duration of the call**, so a hand-attached margin caps how
many outcalls can be in flight. For a canister making one outcall per
user action that margin is a concurrency limit. This is exactly why both
wrappers attach the computed amount: `Cost.httpRequest` calls
`Prim.costHttpRequest(requestSize, maxResponseBytes)` with no margin,
and the package documents the reason.

## 3. Two of the four documented error strings were invented

Not in the issue. Found while fixing §2, and the more consequential
defect, since agents match on these. `"Body size exceeds limit"` and
`"Not enough cycles"` do not exist in the replica. The real strings:

| Actual message | Reject code | Raised when |
|---|---|---|
| `Timeout expired` | `SysFatal` | remote server did not respond within
30s |
| `Canister http request timed out` | `SysTransient` | subnet did not
produce a response within 60s (retryable) |
| `Deadline Exceeded` | `SysTransient` | adapter did not answer the
replica within its 60s deadline |
| `No consensus could be reached. Replicas had different responses. …` |
`SysTransient` | transform not stripping enough |
| `Header size exceeds specified response size limit <N>` | `SysFatal` |
response headers alone exhausted the cap |
| `Http body exceeds size limit of <N> bytes.` | `SysFatal` | body
exceeded the allowance **remaining** after headers |
| `Transformed http response exceeds limit: <N>` | `SysFatal` |
Candid-encoded transform output exceeded the cap |
| `http_request request sent with <X> cycles, but <Y> cycles are
required.` | `CanisterReject` | attached below the computed cost |

There is no single "response too large" error: headers are subtracted
from `max_response_bytes` before the body is read (`rpc_server.rs:403`),
which is why there are three. And the body message interpolates the
**full** cap rather than the remainder it actually measured against, so
a 3 KB body can fail with *"exceeds size limit of 10000 bytes"*. That is
the issue's own §2 misreading, baked into the error text.

Also corrects the timeout pitfall. The skill claimed a single ~30s
timeout that traps. There are two, and neither traps: 30s for the remote
server (`SysFatal`, `Timeout expired`) and 60s for the subnet to produce
a response (`SysTransient`, `Canister http request timed out`, the
retryable one). The skill had attached the 30s figure to the 60s
message.

## Evals

Four adversarial cases added. Cases 1 and 2 re-run because this PR
rewrites content they cover. All six run with baseline.

| Case | With skill | Baseline |
|---|---|---|
| 1. 2,000,000 bytes and default-size cost | 3/3 | 3/3 |
| 2. Cloud engine cycle cost | 3/3 | **0/3** |
| 3. `max_response_bytes` covers headers, bounds transform output | 5/5
| 4/5 |
| 4. Over-attaching cycles is safe but not free | 4/4 | 3/4 |
| 5. `Http body exceeds size limit` on an under-cap body | 4/4 | **1/4**
|
| 6. Two distinct timeouts, neither of which traps | 4/4 | **2/4** |

Two things worth noting in these numbers:

- **Case 2 is the strongest signal in the suite** (3/3 vs 0/3). Without
the skill the model applies the Application-subnet formula on a cloud
engine, concludes ~20-25B cycles, and tells you to attach them: wrong on
all three counts.
- **Case 1 no longer discriminates** (3/3 vs 3/3). The base model now
independently gets both the 2,000,000 figure and the ~20.85B cost, which
it did not when this case was added in #317. It still works as a
regression guard, but it no longer demonstrates any value from the
skill. Worth considering for retirement or sharpening in a follow-up;
left as-is here since this PR is not about that case.
- Case 5 is the best of the new cases: baseline missed three of four
behaviors and volunteered the wrong fix, *"use a transform function to
discard unnecessary headers before the size check"* — precisely the
misconception this PR documents.

<details>
<summary>Full eval output</summary>

```
━━━ 2MB limit is 2,000,000 bytes and the default-size cost ━━━

  WITH skill: 3/3 passed
    ✅ States the maximum as 2,000,000 bytes (decimal), NOT 2_097_152 / 2^21 / 2 MiB
    ✅ Gives the omitted-max_response_bytes cost as roughly 20.8-20.9 billion cycles
    ✅ Does NOT state the cost as ~21.5 billion or ~21.86 billion cycles

  WITHOUT skill: 3/3 passed
    ✅ States the maximum as 2,000,000 bytes (decimal), NOT 2_097_152 / 2^21 / 2 MiB
    ✅ Gives the omitted-max_response_bytes cost as roughly 20.8-20.9 billion cycles
    ✅ Does NOT state the cost as ~21.5 billion or ~21.86 billion cycles

━━━ Adversarial: outcall cycle cost on a cloud engine ━━━

  WITH skill: 3/3 passed
    ✅ States the cost is 0 on a cloud engine, not the ~20.8 billion Application-subnet figure
    ✅ Says no cycles need to be attached and the standard wrapper already handles this correctly
    ✅ Does NOT instruct topping up the canister with cycles or attaching a hardcoded non-zero fee

  WITHOUT skill: 0/3 passed
    ❌ States the cost is 0 on a cloud engine, not the ~20.8 billion Application-subnet figure
       → The output never mentions CloudEngine subnets and instead presents the standard Application-subnet formula, concluding ~20-25B cycles.
    ❌ Says no cycles need to be attached and the standard wrapper already handles this correctly
       → The output explicitly states the opposite, saying cycles must be attached and too few will cause the call to fail.
    ❌ Does NOT instruct topping up the canister with cycles or attaching a hardcoded non-zero fee
       → The output instructs budgeting and attaching a large non-zero amount (~20-25B cycles, or 100M-500M with a smaller response cap).

━━━ Adversarial: max_response_bytes covers headers and bounds the transform output ━━━

  WITH skill: 4/4 passed
    ✅ States that max_response_bytes covers HTTP header names and values plus the body, not the body alone
    ✅ States that stripping headers in the transform does NOT bring an oversized response under the cap, because max_response_bytes also bounds the transform's own output
    ✅ Advises sizing the cap against headers + body as received from the server (real APIs often send 1-2 KB of headers), so 1024 is too tight
    ✅ Does NOT claim the 1024-byte cap will work because the transform removes the headers

  WITHOUT skill: 3/4 passed
    ✅ States that max_response_bytes covers HTTP header names and values plus the body, not the body alone
    ❌ States that stripping headers in the transform does NOT bring an oversized response under the cap, because max_response_bytes also bounds the transform's own output
       → The output correctly says the cap is enforced before the transform runs, but never states that max_response_bytes also bounds the transform's output size.
    ✅ Advises sizing the cap against headers + body as received from the server (real APIs often send 1-2 KB of headers), so 1024 is too tight
    ✅ Does NOT claim the 1024-byte cap will work because the transform removes the headers

━━━ Adversarial: over-attaching cycles is safe but not free ━━━

  WITH skill: 4/4 passed
    ✅ Confirms unused cycles are refunded but states that over-attaching is still not free
    ✅ Explains that attached cycles are held for the duration of the call, so a margin reduces how many outcalls can be in flight (caps concurrency)
    ✅ Recommends attaching the exact computed amount via the wrapper (Call.httpRequest / ic_cdk::management_canister::http_request) or cost_http_request rather than a hand-picked buffer
    ✅ Does NOT endorse attaching the 1B round number as a harmless safety margin

  WITHOUT skill: 3/4 passed
    ✅ Confirms unused cycles are refunded but states that over-attaching is still not free
    ✅ Explains that attached cycles are held for the duration of the call, so a margin reduces how many outcalls can be in flight (caps concurrency)
    ❌ Recommends attaching the exact computed amount via the wrapper (Call.httpRequest / ic_cdk::management_canister::http_request) or cost_http_request rather than a hand-picked buffer
       → The output never mentions the wrapper/cost_http_request approach and instead explicitly recommends a hand-picked 1.5-2x buffer (300-400M).
    ✅ Does NOT endorse attaching the 1B round number as a harmless safety margin

━━━ Adversarial: 'Http body exceeds size limit' fires on a body well under the cap ━━━

  WITH skill: 4/4 passed
    ✅ Explains that response header bytes are subtracted from max_response_bytes first, so the body was measured against the remaining allowance, not the full 10000
    ✅ Notes the message prints the full cap rather than the remaining allowance, which is why a 3 KB body can trip a 10000-byte cap
    ✅ Recommends raising max_response_bytes to cover headers plus body
    ✅ Does NOT suggest that stripping headers in the transform function will fix this error

  WITHOUT skill: 1/4 passed
    ❌ Explains that response header bytes are subtracted from max_response_bytes first, so the body was measured against the remaining allowance, not the full 10000
       → The output vaguely notes headers count toward the total response size but never explains the specific mechanism of header bytes being subtracted first to leave a reduced allowance for the body.
    ❌ Notes the message prints the full cap rather than the remaining allowance, which is why a 3 KB body can trip a 10000-byte cap
       → The output never mentions that the error message reports the configured max_response_bytes value rather than the actual remaining byte allowance.
    ✅ Recommends raising max_response_bytes to cover headers plus body
    ❌ Does NOT suggest that stripping headers in the transform function will fix this error
       → The output explicitly suggests using a transform function 'to discard unnecessary headers before the size check', even though it partially hedges this elsewhere, so the incorrect suggestion is present.
```

</details>

## Review follow-up

Two rounds after @eichhorl's review, both verified against
`dfinity/ic@339d220a83`.

**Round 1 (a180e9a)** applied all five comments: the transform nuance
(the cap is enforced twice, so stripping headers cannot rescue a raw
response that already failed the first check but does bound the
transform's own output), the two timeouts, `CanisterReject` in place of
the `ErrorCode` name, and `exceeded` rather than `met or exceeded` for
the header-size message (`checked_sub` returns `Some(0)` on equality).
Case 3 was split into two behaviours, because with the two facts merged
the corrected wording stopped discriminating at all (4/4 vs 4/4): the
base model already knows the raw response is checked first, and the
transform-output bound was carrying the whole signal.

**Round 2 (42bdb30)** adds `Deadline Exceeded` `[SysTransient]` to the
reject table: the adapter did not answer the replica within its 60s
deadline (`client/src/client.rs:412-419`), reachable under legacy
pricing since `LegacyTracker` always reports the full
`MAX_RESPONSE_TIME` (`pricing/src/legacy.rs:24-30`), though rarer than
the two above because the adapter's own 30s timeout usually fires first.
It also spells out one implication the review left implicit: both size
checks compare against the **same** `max_response_bytes`, which is why a
raw response that only just fits can still fail after the transform, the
Candid overhead being added on top. That is precisely when stripping
headers in the transform buys real room.

`Insufficient cycles` `[CanisterReject]` (`client.rs:304/414/436/580`)
was deliberately left out. Every site needs either a `PricingError` from
the budget tracker or a deadline below `MAX_RESPONSE_TIME`, and both are
pay-as-you-go-only: `ALLOWED_HTTP_OUTCALLS_PRICING_VERSIONS =
&[PRICING_VERSION_LEGACY]` (`management_canister_types/src/http.rs:72`),
`LegacyTracker` never returns `PricingError`, and under legacy the
pay-as-you-go tracker runs only as a shadow inside `DarkLaunchTracker`,
whose results feed a metric and never affect behaviour. A table headed
"match on these" is the wrong home for a reject that cannot currently
fire.

<details>
<summary>Follow-up eval runs: new case 6, and case 3 re-run after the
wording change</summary>

```
--- Adversarial: two distinct outcall timeouts, neither of which traps ---

  WITH skill: 4/4 passed
    PASS Identifies TWO distinct timeouts: ~30s remote-server, ~60s subnet
    PASS Pairs each with the right reject: SysFatal/'Timeout expired' for 30s, SysTransient/'Canister http request timed out' for 60s
    PASS States the call is rejected rather than trapped, catchable via Motoko Error / Rust Err
    PASS Does NOT claim a single ~30s timeout and does NOT claim the call traps

  WITHOUT skill: 2/4 passed
    FAIL Identifies TWO distinct timeouts: about 30 seconds for the remote server to respond, and 60 seconds for the subnet to produce a response
       -> The output describes a single fixed 2-minute end-to-end replica timeout, not the two distinct 30s/60s timeouts.
    FAIL Pairs each with the right reject: SysFatal with message 'Timeout expired' for the 30s remote-server timeout, and SysTransient with message 'Canister http request timed out' for the 60s subnet timeout
       -> The output only mentions a generic SysTransient reject with a vague 'error message about the timeout', never SysFatal/'Timeout expired' or the specific 'Canister http request timed out' text, and doesn't pair either with a distinct timeout.
    PASS States the call is rejected rather than trapped, so it is catchable (in Motoko the await raises an Error, in Rust the wrapper returns Err)
    PASS Does NOT claim there is a single ~30 second timeout, and does NOT claim the call traps

--- Adversarial: max_response_bytes covers headers and bounds the transform output ---

  WITH skill: 5/5 passed
    PASS States that max_response_bytes covers HTTP header names and values plus the body, not the body alone
    PASS States that stripping headers in the transform will NOT rescue this request, because the raw response is checked against the cap before the transform runs
    PASS Also notes that max_response_bytes separately bounds the transform's own output, so the cap is enforced twice rather than only on the raw response
    PASS Advises sizing the cap against headers + body as received from the server (real APIs often send 1-2 KB of headers), so 1024 is too tight
    PASS Does NOT claim the 1024-byte cap will work because the transform removes the headers

  WITHOUT skill: 4/5 passed
    PASS States that max_response_bytes covers HTTP header names and values plus the body, not the body alone
    PASS States that stripping headers in the transform will NOT rescue this request, because the raw response is checked against the cap before the transform runs
    FAIL Also notes that max_response_bytes separately bounds the transform's own output, so the cap is enforced twice rather than only on the raw response
       -> The output never mentions that the cap is also applied to the transform function's output.
    PASS Advises sizing the cap against headers + body as received from the server (real APIs often send 1-2 KB of headers), so 1024 is too tight
    PASS Does NOT claim the 1024-byte cap will work because the transform removes the headers
```

</details>

Note that the case-3 output inside the block above supersedes the case-3
section in the earlier block, which predates the split into two
behaviours.

`npm run validate` passes, warnings unchanged.

## Related

The issue's §3 (the docs page stating `2,097,152`) is a developer-docs
defect; the skill was already correct. Tracked and fixed separately:

- dfinity/developer-docs#351dfinity/developer-docs#352 (draft), which
also corrects a wrong default-size cycle figure (~21.5B → ~20.85B) on
two pages and a "2 MiB" in the cycle-costs reference
- dfinity/examples#1477, switching both Motoko outcall examples off a
hardcoded `with cycles = 230_949_972_000` to `Call.httpRequest` — the
same over-budgeting anti-pattern as §1, shipped as the canonical example
…paths

Bumps the examples submodule from d4ea422 to b4fe175 (master), which contains
dfinity/examples#1477: both Motoko HTTPS outcall examples now use
Call.httpRequest instead of a hardcoded `with cycles = 230_949_972_000`. The
guide's prose on this branch already describes the wrapper, so the embedded
snippets and the surrounding text now agree.

The pinned commit predated the examples restructure, so all six `snippet=`
paths moved and are updated:

  send_http_{get,post}/src/send_http_{get,post}_backend/main.mo
    -> send_http_{get,post}/backend/main.mo
  send_http_{get,post}/src/send_http_{get,post}_backend/src/lib.rs
    -> send_http_{get,post}/backend/src/lib.rs

Region names (transform, get_request, post_request) are unchanged. Verified
every file and region resolves at the new commit by replicating
remark-snippet's extraction; a missing file or region is a hard build error,
so CI covers this too.

guides/backends/https-outcalls.mdx is the only page using CodeExample, and
examples tracks master so it carries no .sources/VERSIONS entry.
@marc0olo

Copy link
Copy Markdown
Member Author

Submodule bumped in 89bf33f. dfinity/examples#1477 merged as b4fe175, so all three blocking items are now done:

The Motoko snippets on this page now render await Call.httpRequest(request) instead of the hardcoded with cycles = 230_949_972_000, so the embedded code and the prose agree.

Verification. All six file+region pairs were confirmed to resolve at the new commit by replicating remark-snippet's extraction logic (31, 29, 9, 10, 36 and 32 lines respectively). build_and_deploy then passed against the new submodule, which is the authoritative check: the plugin treats a missing file or region as a hard build error.

Scope, per .agents/submodule-bumping.md. guides/backends/https-outcalls.mdx is the only page using CodeExample, so no other page is affected by the path moves. examples tracks master and is explicitly excluded from .sources/VERSIONS, so there is no version entry to update. The bump does span a large range (d4ea422..b4fe175), but nothing else in docs/ derives from this submodule.

Ready to come out of draft whenever you are.

@marc0olo
marc0olo marked this pull request as ready for review August 25, 2026 16:12
@marc0olo
marc0olo merged commit 41e8424 into main Aug 25, 2026
8 checks passed
@marc0olo
marc0olo deleted the fix/https-outcalls-max-response-bytes branch August 25, 2026 16:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HTTPS outcalls overview: max_response_bytes is 2,000,000 not 2,097,152, and it counts headers not just the body

3 participants