Skip to content

fix(api): re-raise RateLimitError after exhausting retries#33

Open
guanbinrui wants to merge 1 commit into
masterfrom
fix/retry-reraise-ratelimit
Open

fix(api): re-raise RateLimitError after exhausting retries#33
guanbinrui wants to merge 1 commit into
masterfrom
fix/retry-reraise-ratelimit

Conversation

@guanbinrui

Copy link
Copy Markdown
Member

Problem

Nitter was stuck in a crash → restart → crash loop. Logs showed a SIGSEGV immediately after the server started serving:

INFO Jester is making jokes at http://0.0.0.0:8080
SIGSEGV: Illegal storage access. (Attempt to read from nil?)

…repeating on every restart (restart: unless-stopped).

Root cause

The retry template in src/apiutils.nim swallowed the final RateLimitError instead of propagating it:

for i in 0 ..< maxRetries:        # maxRetries = 1 by default
  try: bod; break
  except RateLimitError:
    echo "..."; await sleepAsync(retryDelayMs)   # exception dies here

On a busy public instance with few sessions, sessions get rate-limited within seconds, so every fetch attempt raises RateLimitError. The loop caught the last one, logged, slept, and fell through without re-raising. fetch then returned its default nil JsonNode (e.g. getGraphUserTweets: js = await fetch(url)), and the parser dereferenced the nil JsonNode's backing tree.

Because the binary is built with -d:danger (Dockerfile), nil checks are off, so this is an uncatchable SIGSEGV rather than a Nim exception — Jester's error RateLimitError / InternalError handlers never run. The process dies and the restart policy loops it forever.

Upstream Nitter avoids this by letting the final RateLimitError propagate so Jester returns an HTTP 429 page.

Fix

Re-raise on the final attempt so the error reaches Jester's RateLimitError handler (HTTP 429), matching upstream semantics. Also guard against maxRetries <= 0 making the loop a no-op.

Result

  • Rate-limit exhaustion now returns a clean 429 "Instance has been rate limited" page instead of segfaulting.
  • No more crash loop.

Note: the underlying operational pressure (few sessions on a public host) still produces 429s under load — add more sessions to reduce them — but the process no longer crashes.

🤖 Generated with Claude Code

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 <noreply@anthropic.com>
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.

1 participant