From 2f549b1d6af6e9acefe112fa9a2da966eb549b34 Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Sat, 6 Jun 2026 00:17:54 +0800 Subject: [PATCH] fix(api): re-raise RateLimitError after exhausting retries The retry template swallowed the final RateLimitError instead of propagating it. When every fetch attempt is rate limited (common on a busy public instance with few sessions), the loop caught the last exception, logged, slept, and fell through without re-raising. fetch then returned its default nil JsonNode, which the parsers dereference. Because the binary is built with -d:danger, that nil access is an uncatchable SIGSEGV ("Attempt to read from nil") rather than a Nim exception, so Jester's RateLimitError handler never runs. The process dies and restart: unless-stopped loops it forever. Re-raise on the final attempt so the error propagates to Jester's RateLimitError handler (HTTP 429), matching upstream semantics. Also guard against maxRetries <= 0 making the loop a no-op. Co-Authored-By: Claude Opus 4.8 --- src/apiutils.nim | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/apiutils.nim b/src/apiutils.nim index d6952e8ca..b58bab42f 100644 --- a/src/apiutils.nim +++ b/src/apiutils.nim @@ -178,13 +178,19 @@ template fetchImpl(result, fetchBody) {.dirty.} = release(session) template retry(bod) = - for i in 0 ..< maxRetries: + let attempts = max(maxRetries, 1) + for i in 0 ..< attempts: try: bod break except RateLimitError: echo "[sessions] Rate limited, retrying ", req.cookie.endpoint, - " request (", i, "/", maxRetries, ")..." + " request (", i + 1, "/", attempts, ")..." + # Re-raise on the final attempt so the error propagates to Jester's + # RateLimitError handler. Swallowing it would let fetch/fetchRaw return + # a nil JsonNode, which the parsers dereference -> SIGSEGV in -d:danger. + if i == attempts - 1: + raise if retryDelayMs > 0: await sleepAsync(retryDelayMs)