Skip to content

Commit b9a93d4

Browse files
fix: parse email verification id in exception (#485)
* Add email_verification_id field to BaseRequestException Fixes #309 When authenticate_with_password() raises an AuthorizationException for an unverified email, the API response includes an email_verification_id field. This field is now properly extracted and accessible on the exception object, eliminating the need to manually parse the response JSON. Co-Authored-By: Deep Singhvi <deep@buildwithfern.com> * Replace base exception field with specific EmailVerificationRequiredException - Created EmailVerificationRequiredException subclass of AuthorizationException - Added email_verification_id field specific to this exception type - Updated HTTP client to detect email_verification_required error code and raise specific exception - Added tests for both the new exception and to verify regular AuthorizationException still works - All 370 tests pass Co-Authored-By: Deep Singhvi <deep@buildwithfern.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 80ef9b9 commit b9a93d4

File tree

3 files changed

+76
-0
lines changed

3 files changed

+76
-0
lines changed

tests/test_sync_http_client.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
BadRequestException,
1111
BaseRequestException,
1212
ConflictException,
13+
EmailVerificationRequiredException,
1314
ServerException,
1415
)
1516
from workos.utils.http_client import SyncHTTPClient
@@ -263,6 +264,57 @@ def test_conflict_exception(self):
263264
assert str(ex) == "(message=No message, request_id=request-123)"
264265
assert ex.__class__ == ConflictException
265266

267+
def test_email_verification_required_exception(self):
268+
request_id = "request-123"
269+
email_verification_id = "email_verification_01J6K4PMSWQXVFGF5ZQJXC6VC8"
270+
271+
self.http_client._client.request = MagicMock(
272+
return_value=httpx.Response(
273+
status_code=403,
274+
json={
275+
"message": "Please verify your email to authenticate via password.",
276+
"code": "email_verification_required",
277+
"email_verification_id": email_verification_id,
278+
},
279+
headers={"X-Request-ID": request_id},
280+
),
281+
)
282+
283+
try:
284+
self.http_client.request("bad_place")
285+
except EmailVerificationRequiredException as ex:
286+
assert (
287+
ex.message == "Please verify your email to authenticate via password."
288+
)
289+
assert ex.code == "email_verification_required"
290+
assert ex.email_verification_id == email_verification_id
291+
assert ex.request_id == request_id
292+
assert ex.__class__ == EmailVerificationRequiredException
293+
assert isinstance(ex, AuthorizationException)
294+
295+
def test_regular_authorization_exception_still_raised(self):
296+
request_id = "request-123"
297+
298+
self.http_client._client.request = MagicMock(
299+
return_value=httpx.Response(
300+
status_code=403,
301+
json={
302+
"message": "You do not have permission to access this resource.",
303+
"code": "forbidden",
304+
},
305+
headers={"X-Request-ID": request_id},
306+
),
307+
)
308+
309+
try:
310+
self.http_client.request("bad_place")
311+
except AuthorizationException as ex:
312+
assert ex.message == "You do not have permission to access this resource."
313+
assert ex.code == "forbidden"
314+
assert ex.request_id == request_id
315+
assert ex.__class__ == AuthorizationException
316+
assert not isinstance(ex, EmailVerificationRequiredException)
317+
266318
def test_request_includes_base_headers(self, capture_and_mock_http_client_request):
267319
request_kwargs = capture_and_mock_http_client_request(self.http_client, {}, 200)
268320

workos/exceptions.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,24 @@ class AuthorizationException(BaseRequestException):
4545
pass
4646

4747

48+
class EmailVerificationRequiredException(AuthorizationException):
49+
"""Raised when email verification is required before authentication.
50+
51+
This exception includes an email_verification_id field that can be used
52+
to retrieve the email verification object or resend the verification email.
53+
"""
54+
55+
def __init__(
56+
self,
57+
response: httpx.Response,
58+
response_json: Optional[Mapping[str, Any]],
59+
) -> None:
60+
super().__init__(response, response_json)
61+
self.email_verification_id = self.extract_from_json(
62+
"email_verification_id", None
63+
)
64+
65+
4866
class AuthenticationException(BaseRequestException):
4967
pass
5068

workos/utils/_base_http_client.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
ServerException,
2121
AuthenticationException,
2222
AuthorizationException,
23+
EmailVerificationRequiredException,
2324
NotFoundException,
2425
BadRequestException,
2526
)
@@ -99,6 +100,11 @@ def _maybe_raise_error_by_status_code(
99100
if status_code == 401:
100101
raise AuthenticationException(response, response_json)
101102
elif status_code == 403:
103+
if (
104+
response_json is not None
105+
and response_json.get("code") == "email_verification_required"
106+
):
107+
raise EmailVerificationRequiredException(response, response_json)
102108
raise AuthorizationException(response, response_json)
103109
elif status_code == 404:
104110
raise NotFoundException(response, response_json)

0 commit comments

Comments
 (0)