Skip to content

fix: timeout Cloudflare cutover API fetches - #167

Merged
steipete merged 3 commits into
openclaw:mainfrom
SebTardif:fix/cutover-fetch-timeout
Sep 5, 2026
Merged

fix: timeout Cloudflare cutover API fetches#167
steipete merged 3 commits into
openclaw:mainfrom
SebTardif:fix/cutover-fetch-timeout

Conversation

@SebTardif

Copy link
Copy Markdown
Contributor

What Problem This Solves

Fixes an issue where operators running the docs hostname cutover would hang until the GitHub runner hit its job limit if the Cloudflare API socket stalled. scripts/cloudflare-cutover-docs-hosts.mjs called api.cloudflare.com with no AbortSignal, so a dry-run or live cutover from pages.yml never returned while the TCP connection sat idle.

Why This Change Was Made

Every Cloudflare API fetch in the cutover script now carries AbortSignal.timeout. The default budget is 30000ms and can be raised with CLOUDFLARE_API_TIMEOUT_MS. Reads and writes share the same helper, so zone lookup, DNS, and Worker route calls all abort. R2 upload, live-smoke, locale commit, and the four workflow timeout-minutes changes from sibling PRs are unchanged.

User Impact

A docs host cutover fails closed on a stalled Cloudflare API request instead of occupying a runner until the workflow times out. Operators can raise the per-request budget when a slow API call needs more than 30 seconds.

Evidence

terminal output from running scripts/cloudflare-cutover-docs-hosts.mjs against the unfixed tree, then the same script after the patch.

Before, a successful zone lookup had no abort signal, and a hung lookup never returned:

$ node --test scripts/cloudflare-cutover-docs-hosts.test.mjs
AssertionError [ERR_ASSERTION]: GET https://api.cloudflare.com/client/v4/zones?name=openclaw.ai&status=active missing AbortSignal
false !== true

AssertionError [ERR_ASSERTION]: hung fetch was killed after 4015ms instead of aborting

After the patch, the same production script attaches an AbortSignal on every Cloudflare API call and aborts a hung request inside the 80ms budget:

$ CLOUDFLARE_API_TOKEN=test-token node --import file:///C:/Users/sebta/AppData/Local/Temp/docs-f006-signal-fetch.mjs scripts/cloudflare-cutover-docs-hosts.mjs --dry-run
{"url":"https://api.cloudflare.com/client/v4/zones?name=openclaw.ai&status=active","method":"GET","hasSignal":true,"signalName":"AbortSignal","signalAborted":false}
zone:openclaw.ai
{"url":"https://api.cloudflare.com/client/v4/zones/zone1/dns_records?name=docs.openclaw.ai&per_page=100","method":"GET","hasSignal":true,"signalName":"AbortSignal","signalAborted":false}
POST:dry-run:/zones/zone1/dns_records
dns:created:docs.openclaw.ai:A
dry-run complete
$ CLOUDFLARE_API_TOKEN=test-token CLOUDFLARE_API_TIMEOUT_MS=80 node --import file:///C:/Users/sebta/AppData/Local/Temp/docs-f006-hung-fetch.mjs scripts/cloudflare-cutover-docs-hosts.mjs --dry-run
live-proof: fetch has AbortSignal aborted=false
Error [AbortError]: The operation was aborted
    at Timeout._onTimeout (node:internal/abort_controller:208:7)
    at listOnTimeout (node:internal/timers:635:17)
    at process.processTimers (node:internal/timers:571:7)
exit=1

node --check scripts/cloudflare-cutover-docs-hosts.mjs exits 0.

This missing abort signal has been present since the cutover script landed in bebb9a9fab (2026-05-23, 105 days). Sibling abort work is #163 and #164 and is not changed here.

Real behavior proof

  • Behavior or issue addressed: A stalled Cloudflare API fetch in the docs host cutover script never timed out, so pages.yml Cut over docs hostnames could occupy a runner until the job was killed.
  • Real environment tested: Windows 11, Node v24.19.0, repo checkout at C:\Users\sebta.grok\tmp\pr-gate-batch\docs-f006 on branch fix/cutover-fetch-timeout, synthetic Cloudflare API via a Node --import preload.
  • Exact steps or command run after this patch: node --check scripts/cloudflare-cutover-docs-hosts.mjs. Then CLOUDFLARE_API_TOKEN=test-token node --import file:///C:/Users/sebta/AppData/Local/Temp/docs-f006-signal-fetch.mjs scripts/cloudflare-cutover-docs-hosts.mjs --dry-run. Then the same production script with CLOUDFLARE_API_TIMEOUT_MS=80 and a hung fetch that only settles when the request signal aborts.
  • Evidence after fix: terminal output above. Every Cloudflare API GET records hasSignal=true and signalName=AbortSignal. The hung request exits 1 after about 145ms with Error [AbortError]: The operation was aborted from AbortSignal.timeout, instead of sitting until the process is killed.
  • Observed result after fix: Cutover fetches now carry a timeout signal. A hung api.cloudflare.com socket fails the script instead of occupying the process until the job is killed.
  • What was not tested: A live Cloudflare zone with production credentials. Slow API calls that need more than the 30s default should set CLOUDFLARE_API_TIMEOUT_MS; that operator path was not run against the real openclaw.ai zone.

Summary

Same abort class as #163 (R2 signed fetch) and #164 (live-smoke fetch), applied to the remaining operator cutover helper. Public path is node scripts/cloudflare-cutover-docs-hosts.mjs from .github/workflows/pages.yml.

The host cutover script called api.cloudflare.com with no AbortSignal.
A stalled API socket could hang the pages.yml cutover step until the
job was killed.

Attach AbortSignal.timeout (default 30000ms, CLOUDFLARE_API_TIMEOUT_MS)
on every Cloudflare API fetch.

Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
Consolidate release notes for the prepared dependency and publishing fixes in the final PR to avoid changelog conflicts. Document the R2 and hostname cutover request budgets.
@steipete

steipete commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Verified final head 236e368b074c6b3a6f2717912768ca24d06102a9.

Ran node scripts/cloudflare-cutover-docs-hosts.mjs --dry-run with the API origin redirected to a loopback HTTP server while retaining native fetch and AbortSignals. Main remained hung until killed after 3,012 ms. This head aborted the stalled request in 130 ms with an 80 ms budget. A healthy full dry run completed 12 reads, all carrying signals, with zero mutations.

The maintainer follow-up rejects malformed and overflowing timeout settings before network operations. All 134 npm test cases passed, including six invalid-setting regressions and the cutover signal/abort tests. actionlint .github/workflows/docs-code-ci.yml and JavaScript syntax checks passed. The complete branch autoreview against origin/main, through P2, is scoped-clean after fixing the timeout parsing finding.

Exact-head checks succeeded:

This PR contains the shared Unreleased entries for #132, #152, #161, #163, #164, #165, #166, and #167, plus the request-timeout documentation. Land it last, after the other seven. All eight changes apply cleanly in that order to main 0b238920811b190f5f685ccf96c31fb09b811f90, verified in an isolated Git index. No production cutover or deployment was performed.

@steipete
steipete merged commit 7e0d2c9 into openclaw:main Sep 5, 2026
5 checks passed
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.

2 participants