forked from ITISFoundation/osparc-simcore
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
🎨 initiate proper error handling for backend clients in api server (I…
- Loading branch information
1 parent
07fff97
commit 71525c3
Showing
17 changed files
with
497 additions
and
387 deletions.
There are no files selected for viewing
3 changes: 1 addition & 2 deletions
3
services/api-server/src/simcore_service_api_server/api/errors/custom_errors.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
55 changes: 22 additions & 33 deletions
55
services/api-server/src/simcore_service_api_server/api/errors/httpx_client_error.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,50 +1,39 @@ | ||
""" General handling of httpx-based exceptions | ||
- httpx-based clients are used to communicate with other backend services | ||
- When those respond with 4XX, 5XX status codes, those are generally handled here | ||
- any exception raised by a httpx client will be handled here. | ||
""" | ||
import logging | ||
from typing import Any | ||
|
||
from fastapi import status | ||
from httpx import HTTPStatusError | ||
from starlette.requests import Request | ||
from starlette.responses import JSONResponse | ||
|
||
from .http_error import create_error_json_response | ||
from fastapi import HTTPException, Request, status | ||
from httpx import HTTPError, TimeoutException | ||
|
||
_logger = logging.getLogger(__file__) | ||
|
||
|
||
async def httpx_client_error_handler(_: Request, exc: HTTPStatusError) -> JSONResponse: | ||
async def handle_httpx_client_exceptions(_: Request, exc: HTTPError): | ||
""" | ||
This is called when HTTPStatusError was raised and reached the outermost handler | ||
This handler is used as a "last resource" since it is recommended to handle these exceptions | ||
closer to the raising point. | ||
The response had an error HTTP status of 4xx or 5xx, and this is how is | ||
transformed in the api-server API | ||
Default httpx exception handler. | ||
See https://www.python-httpx.org/exceptions/ | ||
With this in place only HTTPStatusErrors need to be customized closer to the httpx client itself. | ||
""" | ||
if exc.response.is_client_error: | ||
assert exc.response.is_server_error # nosec | ||
# Forward api-server's client from backend client errors | ||
status_code = exc.response.status_code | ||
errors = exc.response.json()["errors"] | ||
status_code: Any | ||
detail: str | ||
headers: dict[str, str] = {} | ||
if isinstance(exc, TimeoutException): | ||
status_code = status.HTTP_504_GATEWAY_TIMEOUT | ||
detail = f"Request to {exc.request.url.host.capitalize()} timed out" | ||
else: | ||
assert exc.response.is_server_error # nosec | ||
# Hide api-server's client from backend server errors | ||
status_code = status.HTTP_503_SERVICE_UNAVAILABLE | ||
message = f"{exc.request.url.host.capitalize()} service unexpectedly failed" | ||
errors = [ | ||
message, | ||
] | ||
status_code = status.HTTP_502_BAD_GATEWAY | ||
detail = f"{exc.request.url.host.capitalize()} service unexpectedly failed" | ||
|
||
if status_code >= status.HTTP_500_INTERNAL_SERVER_ERROR: | ||
_logger.exception( | ||
"%s. host=%s status-code=%s msg=%s", | ||
message, | ||
"%s. host=%s", | ||
detail, | ||
exc.request.url.host, | ||
exc.response.status_code, | ||
exc.response.text, | ||
) | ||
|
||
return create_error_json_response(*errors, status_code=status_code) | ||
raise HTTPException( | ||
status_code=status_code, detail=detail, headers=headers | ||
) from exc |
2 changes: 1 addition & 1 deletion
2
services/api-server/src/simcore_service_api_server/api/errors/validation_error.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
75 changes: 75 additions & 0 deletions
75
services/api-server/src/simcore_service_api_server/services/_service_exception_handling.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import logging | ||
from contextlib import contextmanager | ||
from functools import wraps | ||
from typing import Any, Callable, Mapping | ||
|
||
import httpx | ||
from fastapi import HTTPException, status | ||
from pydantic import ValidationError | ||
from servicelib.error_codes import create_error_code | ||
|
||
_logger = logging.getLogger(__name__) | ||
|
||
|
||
def service_exception_mapper( | ||
service_name: str, | ||
http_status_map: Mapping[int, tuple[int, Callable[[Any], str] | None]], | ||
): | ||
def decorator(func): | ||
@wraps(func) | ||
async def wrapper(*args, **kwargs): | ||
with backend_service_exception_handler( | ||
service_name, http_status_map, **kwargs | ||
): | ||
return await func(*args, **kwargs) | ||
|
||
return wrapper | ||
|
||
return decorator | ||
|
||
|
||
@contextmanager | ||
def backend_service_exception_handler( | ||
service_name: str, | ||
http_status_map: Mapping[int, tuple[int, Callable[[dict], str] | None]], | ||
**endpoint_kwargs, | ||
): | ||
try: | ||
yield | ||
except ValidationError as exc: | ||
error_code = create_error_code(exc) | ||
_logger.exception( | ||
"Invalid data exchanged with %s service [%s] ", | ||
service_name, | ||
error_code, | ||
extra={"error_code": error_code}, | ||
) | ||
raise HTTPException( | ||
status_code=status.HTTP_502_BAD_GATEWAY, | ||
detail=f"{service_name} service returned invalid response. {error_code}", | ||
) from exc | ||
|
||
except httpx.HTTPStatusError as exc: | ||
if status_detail_tuple := http_status_map.get(exc.response.status_code): | ||
status_code, detail_callback = status_detail_tuple | ||
if detail_callback is None: | ||
detail = f"{exc}" | ||
else: | ||
detail = detail_callback(endpoint_kwargs) | ||
raise HTTPException(status_code=status_code, detail=detail) from exc | ||
if exc.response.status_code in { | ||
status.HTTP_503_SERVICE_UNAVAILABLE, | ||
status.HTTP_429_TOO_MANY_REQUESTS, | ||
}: | ||
headers = {} | ||
if "Retry-After" in exc.response.headers: | ||
headers["Retry-After"] = exc.response.headers["Retry-After"] | ||
raise HTTPException( | ||
status_code=exc.response.status_code, | ||
detail=f"The {service_name} service was unavailable", | ||
headers=headers, | ||
) from exc | ||
raise HTTPException( | ||
status_code=status.HTTP_502_BAD_GATEWAY, | ||
detail=f"Received unexpected response from {service_name}", | ||
) from exc |
Oops, something went wrong.