usestrix/strix · warning · RelayError

invalid_code

invalid_code

Error message

invalid_code

What it means

RelayError('invalid_code') raised by otp_verify() when POST /api/oss/otp/verify returns 403 — the submitted OTP code does not match the one the relay emailed, or it has expired. This is the single 'wrong code' signal from the verification flow.

Solutions

  1. Re-enter the code carefully, copying it verbatim from the newest email (watch for similar glyphs like 0/O, 1/l)
  2. If it may have expired, request a fresh code via otp_start() and use that one within its validity window
  3. Strip whitespace and normalize case before submitting: code.strip().upper() if codes are case-insensitive per relay format
  4. Guard against autofill/old codes: always use the most recent email, not a pinned one

Example fix

# before
otp_verify(email, input('code: '))     # pasted with trailing space -> invalid_code
# after
otp_verify(email, input('code: ').strip())
Defensive patterns

Strategy: try-catch

Validate before calling

import re

def plausible_code(code: str) -> bool:
    c = code.strip()
    return bool(re.fullmatch(r"[0-9A-Za-z]{4,10}", c))

Try / catch

try:
    otp_verify(email, code)
except RelayError as e:
    if e.code == "invalid_code":
        code = prompt("wrong or expired code; re-enter or request a new one").strip()
        otp_verify(email, code)
    else:
        raise

Prevention

When it happens

Trigger: Calling otp_verify(email, '000000') with a mistyped or transposed code; submitting after the code's validity window elapsed; re-submiting a code that was already consumed; case/whitespace corruption when the code is copied from the email or typed by hand.

Common situations: User typo entering the 6-digit code; code expired because the user waited too long before entering it; email client rendering the code with hidden characters; autofill inserting an old code from a previous verification.

Related errors


AI-assisted analysis of usestrix/strix@8551339130 (2026-08-15). Data as JSON: /api/errors/fda5cd85a61b0a58. Report an issue: GitHub.

Appendix: source

Thrown at strix/interface/viewer/auth.py:196

    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
        # fails closed), so treat such a response as a failed verification rather
        # than reporting success and then leaving the user stuck unverified.
        if parse_expiry(data.get("expires_at")) is None:
            raise RelayError("unavailable")
        return data
    if status == 403:
        raise RelayError("invalid_code")
    raise RelayError("unavailable")


def feedback_submit(email: str, message: str) -> None:
    """Relay a feedback message + email to Strix. No verification is required;
    the email is taken as given. Raises RelayError on failure."""
    status, data = _post_json(
        "/api/oss/feedback",
        {"email": email, "message": message},
        timeout=_OTP_TIMEOUT,
    )
    if status == 200:
        return
    if status == 429:
        raise RelayError("rate_limited")
    if status == 400:
        code = data.get("error")
        if code in ("invalid_email", "invalid_message"):

View on GitHub (pinned to 8551339130)