Skip to content

Commit 9f68f8d

Browse files
authored
[Comp-801] Error notification in ui (#726)
1 parent 67d1350 commit 9f68f8d

File tree

13 files changed

+528
-178
lines changed

13 files changed

+528
-178
lines changed

compliance-api/src/compliance_api/__init__.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010
from http import HTTPStatus
1111

1212
import secure
13-
from flask import Flask, current_app, g, request
13+
from flask import Flask, current_app, g, jsonify, request
1414
from flask_cors import CORS
1515
from jose import jwt as jose_jwt
16+
from werkzeug.exceptions import HTTPException
1617

1718
from compliance_api.auth import jwt
1819
from compliance_api.config import get_named_config
@@ -121,13 +122,29 @@ def shutdown_session(exception=None): # pylint: disable=unused-argument
121122
"""Execute teardown actions."""
122123
db.session.remove()
123124

125+
@app.errorhandler(HTTPException)
126+
def handle_http_exception(error):
127+
"""Handle all HTTP exceptions with proper JSON responses."""
128+
response = {
129+
"message": error.description or str(error),
130+
"status": error.code
131+
}
132+
return jsonify(response), error.code
133+
124134
@app.errorhandler(Exception)
125135
def handle_error(err):
136+
"""Handle all other non-HTTP exceptions."""
126137
if run_mode != "production":
127-
# To get stacktrace in local development for internal server errors
138+
# In development, re-raise to see full stacktrace
128139
raise err
129-
current_app.logger.error(str(err))
130-
return "Internal server error", HTTPStatus.INTERNAL_SERVER_ERROR
140+
141+
current_app.logger.error(f"Unhandled exception: {str(err)}", exc_info=True)
142+
143+
response = {
144+
"message": "An internal server error occurred",
145+
"status": 500
146+
}
147+
return jsonify(response), HTTPStatus.INTERNAL_SERVER_ERROR
131148

132149
# Return App for run in run.py file
133150
return app

compliance-api/src/compliance_api/exceptions/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
error - a description of the error {code / description: classname / full text}
2020
status_code - where possible use HTTP Error Codes
2121
"""
22-
from werkzeug.exceptions import BadRequest, Conflict, Forbidden, NotFound, UnprocessableEntity
22+
from werkzeug.exceptions import BadRequest, Conflict, Forbidden, NotFound, ServiceUnavailable, UnprocessableEntity
2323
from werkzeug.wrappers.response import Response
2424

2525

@@ -81,3 +81,13 @@ def __init__(self, message, *args, **kwargs):
8181
super().__init__(*args, **kwargs)
8282
self.description = message
8383
self.response = Response(message, status=UnprocessableEntity.code)
84+
85+
86+
class ServiceUnavailableError(ServiceUnavailable):
87+
"""Exception raised when an external service is unavailable."""
88+
89+
def __init__(self, message, *args, **kwargs):
90+
"""Return a valid ServiceUnavailableError."""
91+
super().__init__(*args, **kwargs)
92+
self.description = message
93+
self.response = Response(message, status=ServiceUnavailable.code)

compliance-api/src/compliance_api/services/authorize_service/auth_service.py

Lines changed: 139 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import requests
44
from flask import current_app, g
55

6-
from compliance_api.exceptions import BusinessError
6+
from compliance_api.exceptions import BadRequestError, BusinessError, ResourceNotFoundError, ServiceUnavailableError
77
from compliance_api.utils.constant import AUTH_APP
88
from compliance_api.utils.enum import HttpMethod
99

@@ -16,45 +16,140 @@ class AuthService:
1616
@staticmethod
1717
def get_epic_user_by_guid(auth_user_guid: str):
1818
"""Return the user representation from epic.authorize."""
19-
auth_user_response = _request_auth_service(f"users/{auth_user_guid}")
20-
if auth_user_response.status_code != 200:
21-
raise BusinessError(
22-
f"Error finding user with ID {auth_user_guid} from auth server"
19+
try:
20+
response = _request_auth_service(f"users/{auth_user_guid}")
21+
22+
if response.status_code == 404:
23+
raise ResourceNotFoundError(
24+
f"User with ID {auth_user_guid} not found in EPIC.auth"
25+
)
26+
27+
if response.status_code != 200:
28+
current_app.logger.error(
29+
f"EPIC.auth returned status {response.status_code} "
30+
f"for user {auth_user_guid}"
31+
)
32+
raise BadRequestError(
33+
"Unable to retrieve user information at this time"
34+
)
35+
36+
return response.json()
37+
38+
except requests.exceptions.RequestException as e:
39+
current_app.logger.error(
40+
f"EPIC.auth service unavailable for user {auth_user_guid}: {str(e)}",
41+
exc_info=True,
42+
)
43+
raise ServiceUnavailableError(
44+
"The user service is temporarily unavailable. Please try again later."
45+
)
46+
47+
except (ResourceNotFoundError, BadRequestError):
48+
raise
49+
50+
except (KeyError, ValueError, TypeError) as e:
51+
current_app.logger.error(
52+
f"Error parsing user data for {auth_user_guid}: {str(e)}",
53+
exc_info=True,
54+
)
55+
raise BadRequestError(
56+
f"Unable to parse user information for {auth_user_guid}"
2357
)
24-
return auth_user_response.json()
2558

2659
@staticmethod
2760
def get_epic_users_by_app():
2861
"""Return all users belonging to COMPLIANCE app."""
29-
auth_users_response = _request_auth_service(f"users?app_name={AUTH_APP}")
30-
if auth_users_response.status_code != 200:
31-
raise BusinessError(f"Error fetching users for the app {AUTH_APP}")
32-
return auth_users_response.json()
62+
try:
63+
response = _request_auth_service(f"users?app_name={AUTH_APP}")
64+
65+
if response.status_code != 200:
66+
current_app.logger.error(
67+
f"EPIC.auth returned status {response.status_code} for app users"
68+
)
69+
raise BadRequestError(
70+
f"Unable to retrieve users for the app {AUTH_APP}"
71+
)
72+
73+
return response.json()
74+
75+
except requests.exceptions.RequestException as e:
76+
current_app.logger.error(
77+
f"EPIC.auth service unavailable for app users: {str(e)}",
78+
exc_info=True,
79+
)
80+
raise ServiceUnavailableError(
81+
"The user service is temporarily unavailable. Please try again later."
82+
)
83+
84+
except BadRequestError:
85+
raise
86+
87+
except (KeyError, ValueError, TypeError) as e:
88+
current_app.logger.error(
89+
f"Error parsing app users response: {str(e)}",
90+
exc_info=True,
91+
)
92+
raise BadRequestError("Unable to parse user information")
3393

3494
@staticmethod
3595
def update_user_group(auth_user_guid: str, payload: dict):
3696
"""Update the group of the user in the identity server."""
37-
update_group_response = _request_auth_service(
38-
f"users/{auth_user_guid}/groups", HttpMethod.PUT, payload
39-
)
40-
if update_group_response.status_code != 204:
41-
raise BusinessError(
42-
f"Update group in the auth server failed for user : {auth_user_guid}"
97+
try:
98+
response = _request_auth_service(
99+
f"users/{auth_user_guid}/groups",
100+
HttpMethod.PUT,
101+
payload,
43102
)
44103

45-
return update_group_response
104+
if response.status_code != 204:
105+
current_app.logger.error(
106+
f"EPIC.auth returned status {response.status_code} "
107+
f"for updating user group {auth_user_guid}"
108+
)
109+
raise BadRequestError(
110+
f"Update group failed for user {auth_user_guid}"
111+
)
112+
113+
return response
114+
115+
except requests.exceptions.RequestException as e:
116+
current_app.logger.error(
117+
f"EPIC.auth service unavailable while updating group "
118+
f"for {auth_user_guid}: {str(e)}",
119+
exc_info=True,
120+
)
121+
raise ServiceUnavailableError(
122+
"The user service is temporarily unavailable. Please try again later."
123+
)
46124

47125
@staticmethod
48126
def delete_user_group(auth_user_guid: str, group: str, del_sub_group_mappings=True):
49127
"""Delete user group."""
50-
delete_response = _request_auth_service(
51-
f"users/{auth_user_guid}/groups/{group}?del_sub_group_mappings={del_sub_group_mappings}",
52-
HttpMethod.DELETE,
53-
)
54-
if delete_response.status_code != 204:
55-
raise BusinessError("Delete group mapping failed")
128+
try:
129+
response = _request_auth_service(
130+
f"users/{auth_user_guid}/groups/{group}"
131+
f"?del_sub_group_mappings={del_sub_group_mappings}",
132+
HttpMethod.DELETE,
133+
)
134+
135+
if response.status_code != 204:
136+
current_app.logger.error(
137+
f"EPIC.auth returned status {response.status_code} "
138+
f"for deleting group {group} for user {auth_user_guid}"
139+
)
140+
raise BadRequestError("Delete group mapping failed")
56141

57-
return delete_response
142+
return response
143+
144+
except requests.exceptions.RequestException as e:
145+
current_app.logger.error(
146+
f"EPIC.auth service unavailable while deleting group "
147+
f"for {auth_user_guid}: {str(e)}",
148+
exc_info=True,
149+
)
150+
raise ServiceUnavailableError(
151+
"The user service is temporarily unavailable. Please try again later."
152+
)
58153

59154

60155
def _request_auth_service(
@@ -73,19 +168,24 @@ def _request_auth_service(
73168

74169
url = f"{auth_base_url}/api/{relative_url}"
75170

76-
if http_method == HttpMethod.GET:
77-
response = requests.get(url, headers=headers, timeout=API_REQUEST_TIMEOUT)
78-
elif http_method == HttpMethod.PUT:
79-
response = requests.put(
80-
url, headers=headers, json=data, timeout=API_REQUEST_TIMEOUT
81-
)
82-
elif http_method == HttpMethod.PATCH:
83-
response = requests.patch(
84-
url, headers=headers, json=data, timeout=API_REQUEST_TIMEOUT
85-
)
86-
elif http_method == HttpMethod.DELETE:
87-
response = requests.delete(url, headers=headers, timeout=API_REQUEST_TIMEOUT)
88-
else:
89-
raise ValueError("Invalid HTTP method")
90-
response.raise_for_status()
171+
try:
172+
if http_method == HttpMethod.GET:
173+
response = requests.get(url, headers=headers, timeout=API_REQUEST_TIMEOUT)
174+
elif http_method == HttpMethod.PUT:
175+
response = requests.put(
176+
url, headers=headers, json=data, timeout=API_REQUEST_TIMEOUT
177+
)
178+
elif http_method == HttpMethod.PATCH:
179+
response = requests.patch(
180+
url, headers=headers, json=data, timeout=API_REQUEST_TIMEOUT
181+
)
182+
elif http_method == HttpMethod.DELETE:
183+
response = requests.delete(url, headers=headers, timeout=API_REQUEST_TIMEOUT)
184+
else:
185+
raise ValueError("Invalid HTTP method")
186+
response.raise_for_status()
187+
except requests.exceptions.RequestException as e:
188+
raise requests.exceptions.RequestException(
189+
f"Error making request to EPIC.track server: {str(e)}"
190+
) from e
91191
return response

compliance-api/src/compliance_api/services/case_file.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,10 @@ def _set_project_parameters(case_file):
420420
setattr(case_file, "regulated_party", project.get("proponent").get("name"))
421421
if not project_id:
422422
project = UnapprovedProjectModel.get_by_case_file_id(case_file.id)
423+
if not project:
424+
raise ResourceNotFoundError(
425+
f"No project information found for case file {case_file.id}"
426+
)
423427
setattr(case_file, "authorization", project.authorization)
424428
setattr(case_file, "type", project.type)
425429
setattr(case_file, "sub_type", project.sub_type)

0 commit comments

Comments
 (0)