{"record":{"id":"cdd3d3c5cb07330c","repo":"usestrix/strix","slug":"invalid-email","errorCode":"invalid_email","errorMessage":"invalid_email","messagePattern":"invalid_email","errorType":"error_code","errorClass":"RelayError","httpStatus":400,"severity":"warning","filePath":"strix/interface/viewer/auth.py","lineNumber":177,"sourceCode":"        data = json.loads(raw or b\"{}\")\n    except json.JSONDecodeError:\n        return {}\n    return data if isinstance(data, dict) else {}\n\n\ndef otp_start(email: str) -> None:\n    \"\"\"Ask the relay to email a verification code. Raises RelayError on failure.\"\"\"\n    status, data = _post_json(\"/api/oss/otp/start\", {\"email\": email}, timeout=_OTP_TIMEOUT)\n    if status == 200:\n        return\n    if status == 429:\n        raise RelayError(\"rate_limited\")\n    if status == 400:\n        # The relay uses 400 both for a malformed address and, separately, to\n        # reject a free/personal email domain (it wants a work email).\n        if data.get(\"error\") == \"work_email_required\":\n            raise RelayError(\"work_email_required\")\n        raise RelayError(\"invalid_email\")\n    raise RelayError(\"unavailable\")\n\n\ndef otp_verify(email: str, code: str) -> dict[str, Any]:\n    \"\"\"Verify a code. Returns ``{token, email, expires_at}`` or raises RelayError.\"\"\"\n    status, data = _post_json(\n        \"/api/oss/otp/verify\",\n        {\"email\": email, \"code\": code},\n        timeout=_OTP_TIMEOUT,\n    )\n    if status == 200 and isinstance(data.get(\"token\"), str):\n        # A token with no usable expiry cannot unlock history locally (the gate\n        # fails closed), so treat such a response as a failed verification rather\n        # than reporting success and then leaving the user stuck unverified.\n        if parse_expiry(data.get(\"expires_at\")) is None:\n            raise RelayError(\"unavailable\")\n        return data\n    if status == 403:","sourceCodeStart":159,"sourceCodeEnd":195,"githubUrl":"https://github.com/usestrix/strix/blob/85513391305171ecc6faffe03da4a8bda5e3febb/strix/interface/viewer/auth.py#L159-L195","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"# before\notp_start(email_field.value)          # ' user@corp.com' -> invalid_email\n# after\nemail = email_field.value.strip()\nlocal, _, domain = email.partition('@')\nif not local or '.' not in domain:\n    show_error('enter a valid work email')\nelse:\n    otp_start(email)","handlingStrategy":"validation","validationCode":"import re\n\nEMAIL_RE = re.compile(r\"^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$\")\n\ndef valid_email(e: str) -> bool:\n    return bool(EMAIL_RE.match(e.strip())) and not any(ord(c) < 0x20 for c in e)","typeGuard":null,"tryCatchPattern":"try:\n    otp_start(email)\nexcept RelayError as e:\n    if e.code == \"invalid_email\":\n        email = prompt_again(\"enter a valid work email\")\n    else:\n        raise","preventionTips":["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"],"tags":["strix","relay","viewer","auth","validation"],"backgroundTag":null,"analyzedSha":"85513391305171ecc6faffe03da4a8bda5e3febb","analyzedAt":"2026-08-15T05:03:57.275Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}