Skip to content

Commit 7f10151

Browse files
committed
feat: Implemented consistent error handling
Signed-off-by: ntkathole <[email protected]>
1 parent 2e5f564 commit 7f10151

File tree

7 files changed

+334
-60
lines changed

7 files changed

+334
-60
lines changed

docs/reference/feature-servers/registry-server.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,36 @@ All endpoints return JSON responses with the following general structure:
801801
- **Not Found (404)**: Requested resource does not exist
802802
- **Internal Server Error (500)**: Server-side error
803803

804+
### Error Handling
805+
806+
The REST API provides consistent error responses with HTTP status codes included in the JSON response body. All error responses follow this format:
807+
808+
```json
809+
{
810+
"status_code": 404,
811+
"detail": "Entity 'user_id' does not exist in project 'demo_project'",
812+
"error_type": "FeastObjectNotFoundException"
813+
}
814+
```
815+
816+
#### Error Response Fields
817+
818+
- **`status_code`**: The HTTP status code (e.g., 404, 422, 500)
819+
- **`detail`**: Human-readable error message describing the issue
820+
- **`error_type`**: The specific type of error that occurred
821+
822+
#### HTTP Status Code Mapping
823+
824+
| HTTP Status | Error Type | Description | Common Causes |
825+
|-------------|------------|-------------|---------------|
826+
| **400** | `HTTPException` | Bad Request | Invalid request format or parameters |
827+
| **401** | `HTTPException` | Unauthorized | Missing or invalid authentication token |
828+
| **403** | `FeastPermissionError` | Forbidden | Insufficient permissions to access the resource |
829+
| **404** | `FeastObjectNotFoundException` | Not Found | Requested entity, feature view, data source, etc. does not exist |
830+
| **422** | `ValidationError` / `RequestValidationError` / `ValueError` / `PushSourceNotFoundException` | Unprocessable Entity | Validation errors, missing required parameters, or invalid input |
831+
| **500** | `InternalServerError` | Internal Server Error | Unexpected server-side errors |
832+
833+
804834
#### Enhanced Response Formats
805835

806836
The REST API now supports enhanced response formats for relationships and pagination:

sdk/python/feast/api/registry/rest/features.py

Lines changed: 27 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from fastapi import APIRouter, Depends, HTTPException, Query
1+
from fastapi import APIRouter, Depends, Query
22

33
from feast.api.registry.rest.codegen_utils import render_feature_code
44
from feast.api.registry.rest.rest_utils import (
@@ -70,52 +70,35 @@ def get_feature(
7070
name=name,
7171
)
7272

73-
try:
74-
try:
75-
response = grpc_call(grpc_handler.GetFeature, req)
76-
except Exception as e:
77-
raise HTTPException(
78-
status_code=404,
79-
detail=f"Feature '{name}' not found in feature view '{feature_view}' in project '{project}'",
80-
) from e
73+
response = grpc_call(grpc_handler.GetFeature, req)
8174

82-
if include_relationships:
83-
response["relationships"] = get_object_relationships(
84-
grpc_handler, "feature", name, project
85-
)
86-
87-
if response:
88-
dtype_str = response.get("type") or response.get("dtype")
89-
value_type_enum = (
90-
_convert_value_type_str_to_value_type(dtype_str.upper())
91-
if dtype_str
92-
else None
93-
)
94-
feast_type = (
95-
from_value_type(value_type_enum) if value_type_enum else None
96-
)
97-
dtype = (
98-
feast_type.__name__
99-
if feast_type and hasattr(feast_type, "__name__")
100-
else "String"
101-
)
102-
context = dict(
103-
name=response.get("name", name),
104-
dtype=dtype,
105-
description=response.get("description", ""),
106-
tags=response.get("tags", response.get("labels", {})) or {},
107-
)
108-
response["featureDefinition"] = render_feature_code(context)
109-
110-
return response
75+
if include_relationships:
76+
response["relationships"] = get_object_relationships(
77+
grpc_handler, "feature", name, project
78+
)
11179

112-
except HTTPException:
113-
raise
114-
except Exception as e:
115-
raise HTTPException(
116-
status_code=500,
117-
detail=f"Internal server error while retrieving feature '{name}' from feature view '{feature_view}' in project '{project}': {str(e)}",
80+
if response:
81+
dtype_str = response.get("type") or response.get("dtype")
82+
value_type_enum = (
83+
_convert_value_type_str_to_value_type(dtype_str.upper())
84+
if dtype_str
85+
else None
11886
)
87+
feast_type = from_value_type(value_type_enum) if value_type_enum else None
88+
dtype = (
89+
feast_type.__name__
90+
if feast_type and hasattr(feast_type, "__name__")
91+
else "String"
92+
)
93+
context = dict(
94+
name=response.get("name", name),
95+
dtype=dtype,
96+
description=response.get("description", ""),
97+
tags=response.get("tags", response.get("labels", {})) or {},
98+
)
99+
response["featureDefinition"] = render_feature_code(context)
100+
101+
return response
119102

120103
@router.get("/features/all")
121104
def list_features_all(

sdk/python/feast/api/registry/rest/lineage.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from typing import Optional
44

5-
from fastapi import APIRouter, Depends, HTTPException, Query
5+
from fastapi import APIRouter, Depends, Query
66

77
from feast.api.registry.rest.rest_utils import (
88
create_grpc_pagination_params,
@@ -84,9 +84,8 @@ def get_object_relationships_path(
8484
"feature",
8585
]
8686
if object_type not in valid_types:
87-
raise HTTPException(
88-
status_code=400,
89-
detail=f"Invalid object_type. Must be one of: {', '.join(valid_types)}",
87+
raise ValueError(
88+
f"Invalid object_type. Must be one of: {', '.join(valid_types)}"
9089
)
9190

9291
req = RegistryServer_pb2.GetObjectRelationshipsRequest(

sdk/python/feast/api/registry/rest/rest_registry_server.py

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,18 @@
22
import logging
33
import re
44

5-
from fastapi import Depends, FastAPI, status
5+
from fastapi import Depends, FastAPI, HTTPException, Request, status
6+
from fastapi.exceptions import RequestValidationError
7+
from fastapi.responses import JSONResponse
8+
from pydantic import ValidationError
69

710
from feast import FeatureStore
811
from feast.api.registry.rest import register_all_routes
12+
from feast.errors import (
13+
FeastObjectNotFoundException,
14+
FeastPermissionError,
15+
PushSourceNotFoundException,
16+
)
917
from feast.permissions.auth.auth_manager import get_auth_manager
1018
from feast.permissions.server.rest import inject_user_details
1119
from feast.permissions.server.utils import (
@@ -62,11 +70,113 @@ def __init__(self, store: FeatureStore):
6270
"X-Frame-Options": "DENY",
6371
},
6472
)
73+
self._add_exception_handlers()
6574
self._add_logging_middleware()
6675
self._add_openapi_security()
6776
self._init_auth()
6877
self._register_routes()
6978

79+
def _add_exception_handlers(self):
80+
"""Add custom exception handlers to include HTTP status codes in JSON responses."""
81+
82+
@self.app.exception_handler(HTTPException)
83+
async def http_exception_handler(request: Request, exc: HTTPException):
84+
return JSONResponse(
85+
status_code=exc.status_code,
86+
content={
87+
"status_code": exc.status_code,
88+
"detail": exc.detail,
89+
"error_type": "HTTPException",
90+
},
91+
)
92+
93+
@self.app.exception_handler(FeastObjectNotFoundException)
94+
async def feast_object_not_found_handler(
95+
request: Request, exc: FeastObjectNotFoundException
96+
):
97+
return JSONResponse(
98+
status_code=404,
99+
content={
100+
"status_code": 404,
101+
"detail": str(exc),
102+
"error_type": "FeastObjectNotFoundException",
103+
},
104+
)
105+
106+
@self.app.exception_handler(FeastPermissionError)
107+
async def feast_permission_error_handler(
108+
request: Request, exc: FeastPermissionError
109+
):
110+
return JSONResponse(
111+
status_code=403,
112+
content={
113+
"status_code": 403,
114+
"detail": str(exc),
115+
"error_type": "FeastPermissionError",
116+
},
117+
)
118+
119+
@self.app.exception_handler(PushSourceNotFoundException)
120+
async def push_source_not_found_handler(
121+
request: Request, exc: PushSourceNotFoundException
122+
):
123+
return JSONResponse(
124+
status_code=422,
125+
content={
126+
"status_code": 422,
127+
"detail": str(exc),
128+
"error_type": "PushSourceNotFoundException",
129+
},
130+
)
131+
132+
@self.app.exception_handler(ValidationError)
133+
async def validation_error_handler(request: Request, exc: ValidationError):
134+
return JSONResponse(
135+
status_code=422,
136+
content={
137+
"status_code": 422,
138+
"detail": str(exc),
139+
"error_type": "ValidationError",
140+
},
141+
)
142+
143+
@self.app.exception_handler(RequestValidationError)
144+
async def request_validation_error_handler(
145+
request: Request, exc: RequestValidationError
146+
):
147+
return JSONResponse(
148+
status_code=422,
149+
content={
150+
"status_code": 422,
151+
"detail": str(exc),
152+
"error_type": "RequestValidationError",
153+
},
154+
)
155+
156+
@self.app.exception_handler(ValueError)
157+
async def value_error_handler(request: Request, exc: ValueError):
158+
return JSONResponse(
159+
status_code=422,
160+
content={
161+
"status_code": 422,
162+
"detail": str(exc),
163+
"error_type": "ValueError",
164+
},
165+
)
166+
167+
@self.app.exception_handler(Exception)
168+
async def generic_exception_handler(request: Request, exc: Exception):
169+
logger.error(f"Unhandled exception: {exc}", exc_info=True)
170+
171+
return JSONResponse(
172+
status_code=500,
173+
content={
174+
"status_code": 500,
175+
"detail": f"Internal server error: {str(exc)}",
176+
"error_type": "InternalServerError",
177+
},
178+
)
179+
70180
def _add_logging_middleware(self):
71181
from fastapi import Request
72182
from starlette.middleware.base import BaseHTTPMiddleware

sdk/python/feast/api/registry/rest/rest_utils.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
from typing import Callable, Dict, List, Optional
22

3-
from fastapi import HTTPException, Query
3+
from fastapi import Query
44
from google.protobuf.json_format import MessageToDict
55

6-
from feast.errors import FeastObjectNotFoundException
6+
from feast.errors import (
7+
FeastObjectNotFoundException,
8+
FeastPermissionError,
9+
PushSourceNotFoundException,
10+
)
711
from feast.protos.feast.registry import RegistryServer_pb2
812

913

@@ -14,10 +18,14 @@ def grpc_call(handler_fn, request):
1418
try:
1519
response = handler_fn(request, context=None)
1620
return MessageToDict(response)
17-
except FeastObjectNotFoundException as e:
18-
raise HTTPException(status_code=404, detail=str(e))
19-
except Exception:
20-
raise HTTPException(status_code=500, detail="Internal server error")
21+
except (
22+
FeastObjectNotFoundException,
23+
FeastPermissionError,
24+
PushSourceNotFoundException,
25+
):
26+
raise
27+
except Exception as e:
28+
raise e
2129

2230

2331
def get_object_relationships(

sdk/python/feast/registry_server.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from feast.base_feature_view import BaseFeatureView
1313
from feast.data_source import DataSource
1414
from feast.entity import Entity
15-
from feast.errors import FeatureViewNotFoundException
15+
from feast.errors import FeastObjectNotFoundException, FeatureViewNotFoundException
1616
from feast.feast_object import FeastObject
1717
from feast.feature_view import FeatureView
1818
from feast.grpc_error_interceptor import ErrorInterceptor
@@ -1205,9 +1205,9 @@ def GetFeature(self, request: RegistryServer_pb2.GetFeatureRequest, context):
12051205
last_updated_timestamp=last_updated_timestamp,
12061206
tags=getattr(feature, "tags", {}),
12071207
)
1208-
context.set_code(grpc.StatusCode.NOT_FOUND)
1209-
context.set_details("Feature not found")
1210-
return Feature()
1208+
raise FeastObjectNotFoundException(
1209+
f"Feature {request.name} not found in feature view {request.feature_view} in project {request.project}"
1210+
)
12111211

12121212

12131213
def start_server(

0 commit comments

Comments
 (0)