usestrix/strix · warning · RelayError

invalid_email

invalid_email

Error message

invalid_email

What it means

RelayError('invalid_email') raised by otp_start() when POST /api/oss/otp/start returns 400 without the work_email_required error code — i.e. the address is syntactically malformed or otherwise rejected as invalid.

Source

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

        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
        # 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:

View on GitHub (pinned to 8551339130)

Solutions

  1. Validate and normalize the address before calling: strip whitespace and verify a single '@' with a non-empty domain
  2. Print the exact string being submitted (repr(email)) to spot invisible characters
  3. Use the same address format that works in normal mail clients — localpart@domain.tld

Example fix

# before
otp_start(email_field.value)          # ' user@corp.com' -> invalid_email
# after
email = email_field.value.strip()
local, _, domain = email.partition('@')
if not local or '.' not in domain:
    show_error('enter a valid work email')
else:
    otp_start(email)
Defensive patterns

Strategy: validation

Validate before calling

import re

EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")

def valid_email(e: str) -> bool:
    return bool(EMAIL_RE.match(e.strip())) and not any(ord(c) < 0x20 for c in e)

Try / catch

try:
    otp_start(email)
except RelayError as e:
    if e.code == "invalid_email":
        email = prompt_again("enter a valid work email")
    else:
        raise

Prevention

When it happens

Trigger: Calling otp_start() with a malformed address: missing '@', empty local part, stray whitespace (' user@corp.com'), or an address the relay's validator refuses. Any 400 whose body error is not 'work_email_required' maps here.

Common situations: UI input not trimmed before submit; copy-paste introducing a leading/trailing space or invisible Unicode character; form validation skipped in scripting; placeholder text accidentally submitted.

Related errors


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