Repository navigation
bug: email-login endpoints return 500 on a malformed body (unauthenticated /api/auth/email/request and /verify) #3676
Description
Summary
The two unauthenticated email-login endpoints, POST /api/auth/email/request and POST /api/auth/email/verify, return 500 Internal Server Error for a malformed body instead of a 4xx. A body missing a required field, a body that is not a JSON object, or a body that is not JSON at all all hit this. Under the integration suite the same failure sometimes surfaces as a dropped connection ("Server disconnected without sending a response") instead of a 500.
Reproduction
curl -s -X POST http://localhost:8000/api/auth/email/request -H 'Content-Type: application/json' -d '{}'
# => 500 Internal Server Error
curl -s -X POST http://localhost:8000/api/auth/email/verify -H 'Content-Type: application/json' -d '{"email":"test@example.com"}'
# => 500 Internal Server Error
curl -s -X POST http://localhost:8000/api/auth/email/verify -H 'Content-Type: application/json' -d 'not json'
# => 500 Internal Server ErrorThe backend log shows an unhandled pydantic_core.ValidationError from EmailLoginVerify(**body).
Context
Both handlers in src/backend/routers/auth.py take a raw Request, then run body = await request.json() and build the model by hand (EmailLoginRequest(**body), EmailLoginVerify(**body)) with no exception handling. A ValidationError, a JSON decode error or a non-dict body therefore escapes as a 500.
Why it matters:
- These are pre-auth, internet-facing endpoints. Any anonymous caller can produce server errors and full tracebacks in the error log on demand. That is log noise and an alerting false-positive source.
- The full integration run fails
tests/test_email_auth.pyintermittently.test_request_code_missing_email_returns_errorandtest_verify_code_invalid_code_returns_401both failed withRemoteProtocolErrorin recent full runs.
src/backend/routers/settings/whitelist.py (add_to_whitelist, #3455) already solves the same shape. It wraps the parse, maps ValidationError to 422, and rejects a non-object body with a one-sentence 422.
Acceptance Criteria
-
POST /api/auth/email/requestreturns 422, not 500, for{}, for a non-object JSON body, and for a non-JSON body. -
POST /api/auth/email/verifyreturns 422, not 500, for a body missingemailorcode, for a non-object JSON body, and for a non-JSON body. - The 422 path for
/requestkeeps the SEC: User and Agent Enumeration via differential API responses #186 enumeration-uniformity guarantee. A malformed body must not reveal anything about whether an address exists. - A malformed body to either endpoint logs no traceback at ERROR level.
- Unit tests cover each malformed-body case for both endpoints.
Technical Notes
- Mirror the guarded parse in
routers/settings/whitelist.py, or move both handlers to typed FastAPI body parameters if/verify's deliberate no-response_modelchoice (bug: /token returns HTTP 200 with an unusable access_token while a 2FA challenge is pending — silent break for password-grant machine clients #2322) allows it. - Check where the per-IP rate limit runs relative to the parse. A malformed body should still count against the per-IP limit, so the parse error path cannot be used to bypass it.
Activity
obasilakis commentedon Oct 19, 2026
/claim
github-actions commentedon Oct 19, 2026
@obasilakis This issue is already assigned to obasilakis. Use
/unclaimfirst if you want to take over.fix(auth): access-request returns 422 for a malformed body (#3676)
fix(auth): email-login endpoints return 422 for a malformed body (#3676…