Skip to content

Distinguish rate-limit state persistence failures from network failures #479

Closed
4 of 4 issues completed

Description

@edithatogo

Evidence

During the authenticated Senseno remediation on 2026-08-09, validateApiAuthMaterial() intermittently returned network-error even though the same cookie material immediately produced HTTP 200 from https://substack.com/api/v1/handle/options using direct fetch. The local rate-limit state was stored under a OneDrive-backed project directory.

withRateLimit() currently encloses the successful fetch, response accounting, and controller.persist() in one try. A local persistence exception therefore enters the network-error catch path, records status 0, retries persistence, and causes requestJson() to return status 0. This conflates local state persistence with transport failure and can corrupt failure/cooldown evidence.

A separate real HTTP 429 was later observed and correctly stopped the mutation window. These two conditions must remain distinguishable.

Options

A. Typed fail-closed persistence error (recommended)

Separate fetch failure from post-response accounting/persistence. Introduce a typed RateLimitStatePersistenceError and propagate it distinctly; do not overwrite the observed HTTP status with 0. Callers remain fail-closed, but diagnostics say local persistence failed and retain the successful response status in redacted evidence.

Trade-off: safest semantics and auditability, but extends result/error contracts and CLI rendering.

B. Bounded local persistence retry plus typed terminal error

Add a short bounded retry for transient Windows/OneDrive write failures, then use Option A if it still fails.

Trade-off: improves Windows reliability, but needs deterministic tests and must not delay or retry the HTTP request itself.

C. Best-effort persistence

Return the successful HTTP response even when persistence fails and warn.

Trade-off: simplest user experience, but unsafe for mutation workflows because the next process may lose an active cooldown. Not recommended as the default.

Recommendation and fallback

Implement B with A as the terminal behavior. Keep mutation callers fail-closed. If cooldown state cannot be persisted, block further writes and surface a remediation message; read-only callers may expose the successful HTTP status but must mark cooldown persistence as degraded. Do not label either condition as a network failure.

Acceptance criteria

  • A successful HTTP response followed by a mocked writeFile failure is not reported as status 0/network-error.
  • The observed HTTP status and redacted rate-limit headers remain available in the error/receipt.
  • Persistence retries never repeat the HTTP request.
  • Mutation paths fail closed when cooldown state cannot be durably stored.
  • Concurrent-process and transient Windows/OneDrive failure tests cover the state file.
  • A genuine fetch failure and a genuine HTTP 429 retain their existing distinct behavior.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions