fix(pilot): drop pydantic EmailStr — email-validator dep missing → backend startup crash #337

Merged
lekss361 merged 1 commit from fix/pilot-email-validator-hotfix into main 2026-05-17 21:32:24 +00:00
Owner

🔴 Hotfix — backend down on prod

Deploy после merge PR #330 (SF-B3 pilot) завис: Container backend-1 unhealthy → dependency failed to start → live https://gendsgn.ru/healthHTTP 502.

Root cause

backend/app/api/v1/pilot.py:14 импортирует EmailStr из pydantic:

from pydantic import BaseModel, EmailStr, Field

Pydantic v2 требует package email-validator для EmailStr. Он не добавлен в pyproject.toml. При import pilot.py → PydanticImportError: \email-validator` is not installed` → uvicorn не стартует → docker-compose health-check fail.

Trade-In (TI-1 PR #316) тоже использовал pydantic schemas, но без EmailStr, поэтому до сегодня всё работало.

Fix

Drop EmailStr → plain str с минимальным regex pattern ^[^@\s]+@[^@\s]+\.[^@\s]+$.

-from pydantic import BaseModel, EmailStr, Field
+from pydantic import BaseModel, Field
...
-    email: EmailStr | None = None
+    email: str | None = Field(default=None, max_length=200,
+        pattern=r"^[^@\s]+@[^@\s]+\.[^@\s]+$")

Полная email validation может быть на frontend/CRM side.

Alternative (NOT taken)

Add email-validator>=2.0 to pyproject.toml. Worse — больше runtime weight для marginal value (frontend всё равно validate'ит).

Test plan

  • Migration не trog'нута → safe re-deploy
  • После deploy: curl https://gendsgn.ru/health → 200
  • curl -X POST .../pilot/request {"name":"X","email":"a@b.c"} → 200
  • Invalid email "not-an-email" → 422
## 🔴 Hotfix — backend down on prod Deploy после merge PR #330 (SF-B3 pilot) завис: `Container backend-1 unhealthy → dependency failed to start` → live `https://gendsgn.ru/health` → **HTTP 502**. ## Root cause `backend/app/api/v1/pilot.py:14` импортирует `EmailStr` из pydantic: ```python from pydantic import BaseModel, EmailStr, Field ``` **Pydantic v2 требует package `email-validator`** для `EmailStr`. Он **не добавлен в pyproject.toml**. При import pilot.py → `PydanticImportError: \`email-validator\` is not installed` → uvicorn не стартует → docker-compose health-check fail. Trade-In (TI-1 PR #316) тоже использовал pydantic schemas, но без `EmailStr`, поэтому до сегодня всё работало. ## Fix Drop `EmailStr` → plain `str` с минимальным regex pattern `^[^@\s]+@[^@\s]+\.[^@\s]+$`. ```diff -from pydantic import BaseModel, EmailStr, Field +from pydantic import BaseModel, Field ... - email: EmailStr | None = None + email: str | None = Field(default=None, max_length=200, + pattern=r"^[^@\s]+@[^@\s]+\.[^@\s]+$") ``` Полная email validation может быть на frontend/CRM side. ## Alternative (NOT taken) Add `email-validator>=2.0` to `pyproject.toml`. Worse — больше runtime weight для marginal value (frontend всё равно validate'ит). ## Test plan - [ ] Migration не trog'нута → safe re-deploy - [ ] После deploy: `curl https://gendsgn.ru/health` → 200 - [ ] `curl -X POST .../pilot/request {"name":"X","email":"a@b.c"}` → 200 - [ ] Invalid email `"not-an-email"` → 422
lekss361 added 1 commit 2026-05-17 21:25:19 +00:00
Root cause: PR #330 SF-B3 introduced pydantic EmailStr import in
backend/app/api/v1/pilot.py at module level. EmailStr requires the
email-validator package which is not in pyproject deps, so backend import
crashed:

  pydantic.errors.PydanticImportError: `email-validator` is not installed

Result: backend container started but uvicorn import failed → /health
endpoint never bound → docker-compose marked container unhealthy → deploy
exited 1.

Fix: drop EmailStr, use plain `str | None` with a minimal regex pattern.
Strict email validation can run on the frontend or CRM side.
Author
Owner

Deep Code Review — verdict: APPROVE (hotfix)

Status: APPROVE — prod-down hotfix, merging immediately.

Diff sanity

  • Confirmed exact 1-file change in backend/app/api/v1/pilot.py (+5/-2). No surprise edits.
  • Drops EmailStr import + replaces email: EmailStr | None with str | None + Field(max_length=200, pattern=r"^[^@\s]+@[^@\s]+\.[^@\s]+$").

Other EmailStr leakage check

  • Grep EmailStr across backend/app/** → only match is the new comment line in pilot.py:28. No other endpoint imports it. Backend will start after this merge.
  • Grep email-validator|email_validator → no other usage. No dep stub needed.

Regex sanity

  • ^[^@\s]+@[^@\s]+\.[^@\s]+$:
    • Matches a@b.c, user.name+tag@sub.domain.co.uk (greedy [^@\s]+ consumes sub.domain.co then . then uk).
    • Rejects not-an-email (no @), a@b (no dot after @), a @b.c (space), a@@b.c (matches [^@\s] not @).
    • No catastrophic backtracking: each [^@\s]+ is anchored by a literal (@, ., $), no nested quantifier overlap. Safe for untrusted input.
    • max_length=200 caps DoS surface.
  • API contract narrowing: EmailStr accepts user@localhost (no TLD); regex rejects it. Acceptable for lead form (real users have TLDs).

Blast radius

  • Single-file FastAPI schema change. No DB migration, no Celery task, no frontend contract break (response shape unchanged, request still accepts string).
  • Reversibility: easy revert if 422s spike on prod traffic (alternative path = add email-validator>=2.0 to pyproject.toml).

Pre-merge gates

  • mergeable=true, draft=false, state=open
  • Head SHA 31e686e4 matches review target
  • Base = main @ 0567ad21

Merging via squash. Post-deploy verify: curl https://gendsgn.ru/health → 200, POST /api/v1/pilot/request with valid + invalid email.

## Deep Code Review — verdict: APPROVE (hotfix) **Status**: APPROVE — prod-down hotfix, merging immediately. ### Diff sanity - Confirmed exact 1-file change in `backend/app/api/v1/pilot.py` (+5/-2). No surprise edits. - Drops `EmailStr` import + replaces `email: EmailStr | None` with `str | None + Field(max_length=200, pattern=r"^[^@\s]+@[^@\s]+\.[^@\s]+$")`. ### Other `EmailStr` leakage check - Grep `EmailStr` across `backend/app/**` → only match is the new comment line in `pilot.py:28`. No other endpoint imports it. **Backend will start after this merge.** - Grep `email-validator|email_validator` → no other usage. No dep stub needed. ### Regex sanity - `^[^@\s]+@[^@\s]+\.[^@\s]+$`: - Matches `a@b.c`, `user.name+tag@sub.domain.co.uk` (greedy `[^@\s]+` consumes `sub.domain.co` then `.` then `uk`). - Rejects `not-an-email` (no `@`), `a@b` (no dot after `@`), `a @b.c` (space), `a@@b.c` (matches `[^@\s]` not `@`). - **No catastrophic backtracking**: each `[^@\s]+` is anchored by a literal (`@`, `.`, `$`), no nested quantifier overlap. Safe for untrusted input. - `max_length=200` caps DoS surface. - API contract narrowing: `EmailStr` accepts `user@localhost` (no TLD); regex rejects it. Acceptable for lead form (real users have TLDs). ### Blast radius - Single-file FastAPI schema change. No DB migration, no Celery task, no frontend contract break (response shape unchanged, request still accepts string). - Reversibility: easy revert if 422s spike on prod traffic (alternative path = add `email-validator>=2.0` to `pyproject.toml`). ### Pre-merge gates - `mergeable=true`, `draft=false`, `state=open` ✅ - Head SHA `31e686e4` matches review target ✅ - Base = main @ `0567ad21` ✅ Merging via `squash`. Post-deploy verify: `curl https://gendsgn.ru/health` → 200, `POST /api/v1/pilot/request` with valid + invalid email.
lekss361 merged commit 76112870e2 into main 2026-05-17 21:32:24 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#337
No description provided.