Skip to content

fix(http): retry transient public reads and type Cloudflare IP blocks - #17

Merged
MartinStuebler merged 1 commit into
mainfrom
fix/substack-cloudflare-retry
Sep 24, 2026
Merged

MartinStuebler merged 1 commit into
mainfrom
fix/substack-cloudflare-retry

Conversation

@nori-sessions

@nori-sessions nori-sessions Bot commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

Why

The 3-hourly @theahura Notes → X syndication trigger intermittently dies at its first Substack call: some trigger machines get an egress IP that Substack's Cloudflare edge challenges on every request (HTTP 403, cf-mitigated: challenge; a plain curl gets the same). Transcript history shows ~6 blocked runs out of ~250 in the last month, each clearing by the next run. Today the CLI fails on the first 403 with a generic HTTP_ERROR, so agents can't tell an IP block from a real auth/endpoint failure.

What

  • publicGet in src/core/http.ts: public GETs retry network errors, 5xx, and Cloudflare challenges with exponential backoff (NORI_SUBSTACK_HTTP_RETRIES, default 2; NORI_SUBSTACK_HTTP_RETRY_DELAY_MS, default 5000 → 5s, 10s).
  • A challenge that outlasts the retries throws the typed, retry-safe CLOUDFLARE_CHALLENGE error (exit 8) with status, cfMitigated, attempts.
  • RSS and homepage reads in PublicClient go through the same path.
  • Unchanged: plain 403s aren't retried (the publication→homepage fallback still works), 429 keeps its RATE_LIMITED envelope, and authenticated/mutating requests are untouched.
  • The CLI never tries to solve or bypass the challenge.

Not included

No browser fallback. The block is on the machine's IP, so a browser on the same machine gets the same challenge, and getting past it would mean solving it, which we don't do.

Testing

  • New tests/public-retry.test.ts: challenge then success, persistent challenge → CLOUDFLARE_CHALLENGE after 3 attempts, 5xx retry, plain 403 not retried.
  • Full suite: 64/64 passing; lint clean; dist/ recompiled.
  • Live: note export --user-id 9744387 --lookback-hours 0 and post list --publication-url https://12gramsofcarbon.com pass against real Substack.
    🤖 Generated with Nori

Co-Authored-By: Nori contact@tilework.tech

Some session machines get an egress IP that Substack's Cloudflare edge
challenges (403 + cf-mitigated: challenge) on every request. Public GETs
now retry network errors, 5xx, and challenges with exponential backoff,
and a challenge that outlasts the retries surfaces as the retry-safe
CLOUDFLARE_CHALLENGE error instead of a generic HTTP_ERROR 403.
@MartinStuebler
MartinStuebler merged commit 80ff2b1 into main Sep 24, 2026
4 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