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
- Validate and normalize the address before calling: strip whitespace and verify a single '@' with a non-empty domain
- Print the exact string being submitted (repr(email)) to spot invisible characters
- 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
- Strip and validate with a regex before calling otp_start
- Log repr(email) to expose invisible characters from copy-paste
- Validate at the form layer, not only at the relay
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.