usestrix/strix · warning · RelayError
rate_limited
rate_limited
Error message
rate_limited
What it means
RelayError('rate_limited') raised by otp_start() when the relay responds 429 to POST /api/oss/otp/start. The relay throttles how often verification codes can be requested per email/IP, so repeated code requests within a short window are rejected.
Source
Thrown at strix/interface/viewer/auth.py:171
logger.warning("relay request to %s failed: %s", path, exc)
raise RelayError("unavailable") from exc
def _parse_body(raw: bytes) -> dict[str, Any]:
try:
data = json.loads(raw or b"{}")
except json.JSONDecodeError:
return {}
return data if isinstance(data, dict) else {}
def otp_start(email: str) -> None:
"""Ask the relay to email a verification code. Raises RelayError on failure."""
status, data = _post_json("/api/oss/otp/start", {"email": email}, timeout=_OTP_TIMEOUT)
if status == 200:
return
if status == 429:
raise RelayError("rate_limited")
if status == 400:
# The relay uses 400 both for a malformed address and, separately, to
# reject a free/personal email domain (it wants a work email).
if data.get("error") == "work_email_required":
raise RelayError("work_email_required")
raise RelayError("invalid_email")
raise RelayError("unavailable")
def otp_verify(email: str, code: str) -> dict[str, Any]:
"""Verify a code. Returns ``{token, email, expires_at}`` or raises RelayError."""
status, data = _post_json(
"/api/oss/otp/verify",
{"email": email, "code": code},
timeout=_OTP_TIMEOUT,
)
if status == 200 and isinstance(data.get("token"), str):
# A token with no usable expiry cannot unlock history locally (the gateView on GitHub (pinned to 8551339130)
Solutions
- Wait for the relay's cooldown (typically 30-60s) before requesting a new code
- Check the inbox (and spam folder) for an already-sent code before resending
- In scripts, cache the 'code requested' state and only call otp_verify until the code expires
- Cap retries with exponential backoff instead of immediate re-request
Example fix
# before
for attempt in range(5):
otp_start(email) # may 429 immediately
# after
otp_start(email)
for attempt in range(5):
try:
otp_verify(email, input('code: ')); break
except RelayError as e:
if e.code != 'invalid_code': raise
time.sleep(2 ** attempt) Defensive patterns
Strategy: retry
Validate before calling
# Track last request time; only call otp_start after the cooldown.
import time
_last_otp_request = 0.0
OTP_COOLDOWN = 60
def safe_otp_start(email):
global _last_otp_request
wait = OTP_COOLDOWN - (time.monotonic() - _last_otp_request)
if wait > 0:
raise RuntimeError(f"wait {wait:.0f}s before requesting a new code")
_last_otp_request = time.monotonic()
return otp_start(email) Try / catch
try:
otp_start(email)
except RelayError as e:
if e.code == "rate_limited":
show("Code requested too often. Wait a minute, check your inbox, then retry.")
else:
raise Prevention
- Request a code once, then poll the inbox — never re-request on verify failure
- Cache 'code requested' state in scripts instead of blind retries
- Disable resend buttons for the cooldown window in UIs
When it happens
Trigger: Calling otp_start(email) more than the relay's allowed frequency — e.g. clicking 'resend code' several times, or a retry loop that re-requests a code on every failure. The 429 maps directly to RelayError('rate_limited').
Common situations: Impatient users re-clicking the email-verification button; scripts that call otp_start on each attempt instead of waiting; multiple people behind one NAT/IP triggering shared rate limits; automated tests hammering the endpoint.
Related errors
AI-assisted analysis of usestrix/strix@8551339130 (2026-08-15).
Data as JSON: /api/errors/cb365a6bec73467e.
Report an issue: GitHub.