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 gate

View on GitHub (pinned to 8551339130)

Solutions

  1. Wait for the relay's cooldown (typically 30-60s) before requesting a new code
  2. Check the inbox (and spam folder) for an already-sent code before resending
  3. In scripts, cache the 'code requested' state and only call otp_verify until the code expires
  4. 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

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.