From 1574bd2f78a39eb512afcca259bb7e5eb9fd08bc Mon Sep 17 00:00:00 2001 From: bhudevbhanpuriya Date: Tue, 21 Apr 2026 14:47:35 +0000 Subject: [PATCH] fix: correct error handling in read endpoints to return proper HTTP status codes - Replace broad 'except Exception' with specific exception handlers - Return 404 only for StopAsyncIteration (no results found) - Return 503 for database connection errors - Return 500 for unexpected internal errors - Add logging for all exceptions to improve observability - Apply fix consistently across all 10 read endpoints Fixes issue where all runtime failures were incorrectly mapped to 404, making it impossible to distinguish between legitimate 'not found' and backend/service errors. --- api/app/v1/endpoints/read/commit.py | 29 ++++++++++++++++++- api/app/v1/endpoints/read/datastream.py | 29 ++++++++++++++++++- .../v1/endpoints/read/feature_of_interest.py | 29 ++++++++++++++++++- .../v1/endpoints/read/historical_location.py | 29 ++++++++++++++++++- api/app/v1/endpoints/read/location.py | 29 ++++++++++++++++++- api/app/v1/endpoints/read/network.py | 29 ++++++++++++++++++- api/app/v1/endpoints/read/observation.py | 29 ++++++++++++++++++- .../v1/endpoints/read/observed_property.py | 29 ++++++++++++++++++- api/app/v1/endpoints/read/sensor.py | 29 ++++++++++++++++++- api/app/v1/endpoints/read/thing.py | 29 ++++++++++++++++++- 10 files changed, 280 insertions(+), 10 deletions(-) diff --git a/api/app/v1/endpoints/read/commit.py b/api/app/v1/endpoints/read/commit.py index bd9b5941..2cd404e5 100644 --- a/api/app/v1/endpoints/read/commit.py +++ b/api/app/v1/endpoints/read/commit.py @@ -13,7 +13,9 @@ # limitations under the License. import json +import logging +import asyncpg from app import ANONYMOUS_VIEWER, AUTHORIZATION, REDIS from app.db.asyncpg_db import get_pool from app.db.redis_db import redis @@ -24,6 +26,8 @@ from .query_parameters import CommonQueryParams, get_common_query_params from .read import asyncpg_stream_results, wrapped_result_generator +logger = logging.getLogger(__name__) + v1 = APIRouter() user = Header(default=None, include_in_schema=False) @@ -97,7 +101,7 @@ async def get_commits( media_type="application/json", status_code=status.HTTP_200_OK, ) - except Exception as e: + except StopAsyncIteration: return JSONResponse( status_code=status.HTTP_404_NOT_FOUND, content={ @@ -106,6 +110,29 @@ async def get_commits( "message": "Not Found", }, ) + except ( + asyncpg.PostgresConnectionError, + asyncpg.TooManyConnectionsError, + ): + logger.exception("Database unavailable in get_commits") + return JSONResponse( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + content={ + "code": 503, + "type": "error", + "message": "Database temporarily unavailable", + }, + ) + except Exception: + logger.exception("Unexpected streaming error in get_commits") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={ + "code": 500, + "type": "error", + "message": "Internal server error", + }, + ) except Exception as e: return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, diff --git a/api/app/v1/endpoints/read/datastream.py b/api/app/v1/endpoints/read/datastream.py index ac9b253a..358c5dad 100644 --- a/api/app/v1/endpoints/read/datastream.py +++ b/api/app/v1/endpoints/read/datastream.py @@ -13,7 +13,9 @@ # limitations under the License. import json +import logging +import asyncpg from app import ANONYMOUS_VIEWER, AUTHORIZATION, REDIS from app.db.asyncpg_db import get_pool from app.db.redis_db import redis @@ -24,6 +26,8 @@ from .query_parameters import CommonQueryParams, get_common_query_params from .read import asyncpg_stream_results, wrapped_result_generator +logger = logging.getLogger(__name__) + v1 = APIRouter() user = Header(default=None, include_in_schema=False) @@ -96,7 +100,7 @@ async def get_datastreams( media_type="application/json", status_code=status.HTTP_200_OK, ) - except Exception as e: + except StopAsyncIteration: return JSONResponse( status_code=status.HTTP_404_NOT_FOUND, content={ @@ -105,6 +109,29 @@ async def get_datastreams( "message": "Not Found", }, ) + except ( + asyncpg.PostgresConnectionError, + asyncpg.TooManyConnectionsError, + ): + logger.exception("Database unavailable in get_datastreams") + return JSONResponse( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + content={ + "code": 503, + "type": "error", + "message": "Database temporarily unavailable", + }, + ) + except Exception: + logger.exception("Unexpected streaming error in get_datastreams") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={ + "code": 500, + "type": "error", + "message": "Internal server error", + }, + ) except Exception as e: return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, diff --git a/api/app/v1/endpoints/read/feature_of_interest.py b/api/app/v1/endpoints/read/feature_of_interest.py index cc14f9a5..8dab2fe2 100644 --- a/api/app/v1/endpoints/read/feature_of_interest.py +++ b/api/app/v1/endpoints/read/feature_of_interest.py @@ -13,7 +13,9 @@ # limitations under the License. import json +import logging +import asyncpg from app import ANONYMOUS_VIEWER, AUTHORIZATION, REDIS from app.db.asyncpg_db import get_pool from app.db.redis_db import redis @@ -24,6 +26,8 @@ from .query_parameters import CommonQueryParams, get_common_query_params from .read import asyncpg_stream_results, wrapped_result_generator +logger = logging.getLogger(__name__) + v1 = APIRouter() user = Header(default=None, include_in_schema=False) @@ -96,7 +100,7 @@ async def get_features_of_interest( media_type="application/json", status_code=status.HTTP_200_OK, ) - except Exception as e: + except StopAsyncIteration: return JSONResponse( status_code=status.HTTP_404_NOT_FOUND, content={ @@ -105,6 +109,29 @@ async def get_features_of_interest( "message": "Not Found", }, ) + except ( + asyncpg.PostgresConnectionError, + asyncpg.TooManyConnectionsError, + ): + logger.exception("Database unavailable in get_features_of_interest") + return JSONResponse( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + content={ + "code": 503, + "type": "error", + "message": "Database temporarily unavailable", + }, + ) + except Exception: + logger.exception("Unexpected streaming error in get_features_of_interest") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={ + "code": 500, + "type": "error", + "message": "Internal server error", + }, + ) except Exception as e: return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, diff --git a/api/app/v1/endpoints/read/historical_location.py b/api/app/v1/endpoints/read/historical_location.py index aac2c451..8c8377ec 100644 --- a/api/app/v1/endpoints/read/historical_location.py +++ b/api/app/v1/endpoints/read/historical_location.py @@ -13,7 +13,9 @@ # limitations under the License. import json +import logging +import asyncpg from app import ANONYMOUS_VIEWER, AUTHORIZATION, REDIS from app.db.asyncpg_db import get_pool from app.db.redis_db import redis @@ -24,6 +26,8 @@ from .query_parameters import CommonQueryParams, get_common_query_params from .read import asyncpg_stream_results, wrapped_result_generator +logger = logging.getLogger(__name__) + v1 = APIRouter() user = Header(default=None, include_in_schema=False) @@ -96,7 +100,7 @@ async def get_historical_locations( media_type="application/json", status_code=status.HTTP_200_OK, ) - except Exception as e: + except StopAsyncIteration: return JSONResponse( status_code=status.HTTP_404_NOT_FOUND, content={ @@ -105,6 +109,29 @@ async def get_historical_locations( "message": "Not Found", }, ) + except ( + asyncpg.PostgresConnectionError, + asyncpg.TooManyConnectionsError, + ): + logger.exception("Database unavailable in get_historical_locations") + return JSONResponse( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + content={ + "code": 503, + "type": "error", + "message": "Database temporarily unavailable", + }, + ) + except Exception: + logger.exception("Unexpected streaming error in get_historical_locations") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={ + "code": 500, + "type": "error", + "message": "Internal server error", + }, + ) except Exception as e: return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, diff --git a/api/app/v1/endpoints/read/location.py b/api/app/v1/endpoints/read/location.py index 7be3e322..ece99b2c 100644 --- a/api/app/v1/endpoints/read/location.py +++ b/api/app/v1/endpoints/read/location.py @@ -13,7 +13,9 @@ # limitations under the License. import json +import logging +import asyncpg from app import ANONYMOUS_VIEWER, AUTHORIZATION, REDIS from app.db.asyncpg_db import get_pool from app.db.redis_db import redis @@ -24,6 +26,8 @@ from .query_parameters import CommonQueryParams, get_common_query_params from .read import asyncpg_stream_results, wrapped_result_generator +logger = logging.getLogger(__name__) + v1 = APIRouter() user = Header(default=None, include_in_schema=False) @@ -96,7 +100,7 @@ async def get_locations( media_type="application/json", status_code=status.HTTP_200_OK, ) - except Exception as e: + except StopAsyncIteration: return JSONResponse( status_code=status.HTTP_404_NOT_FOUND, content={ @@ -105,6 +109,29 @@ async def get_locations( "message": "Not Found", }, ) + except ( + asyncpg.PostgresConnectionError, + asyncpg.TooManyConnectionsError, + ): + logger.exception("Database unavailable in get_locations") + return JSONResponse( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + content={ + "code": 503, + "type": "error", + "message": "Database temporarily unavailable", + }, + ) + except Exception: + logger.exception("Unexpected streaming error in get_locations") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={ + "code": 500, + "type": "error", + "message": "Internal server error", + }, + ) except Exception as e: return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, diff --git a/api/app/v1/endpoints/read/network.py b/api/app/v1/endpoints/read/network.py index ed688da6..cf651486 100644 --- a/api/app/v1/endpoints/read/network.py +++ b/api/app/v1/endpoints/read/network.py @@ -13,7 +13,9 @@ # limitations under the License. import json +import logging +import asyncpg from app import ANONYMOUS_VIEWER, AUTHORIZATION, REDIS from app.db.asyncpg_db import get_pool from app.db.redis_db import redis @@ -24,6 +26,8 @@ from .query_parameters import CommonQueryParams, get_common_query_params from .read import asyncpg_stream_results, wrapped_result_generator +logger = logging.getLogger(__name__) + v1 = APIRouter() user = Header(default=None, include_in_schema=False) @@ -96,7 +100,7 @@ async def get_networks( media_type="application/json", status_code=status.HTTP_200_OK, ) - except Exception as e: + except StopAsyncIteration: return JSONResponse( status_code=status.HTTP_404_NOT_FOUND, content={ @@ -105,6 +109,29 @@ async def get_networks( "message": "Not Found", }, ) + except ( + asyncpg.PostgresConnectionError, + asyncpg.TooManyConnectionsError, + ): + logger.exception("Database unavailable in get_networks") + return JSONResponse( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + content={ + "code": 503, + "type": "error", + "message": "Database temporarily unavailable", + }, + ) + except Exception: + logger.exception("Unexpected streaming error in get_networks") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={ + "code": 500, + "type": "error", + "message": "Internal server error", + }, + ) except Exception as e: return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, diff --git a/api/app/v1/endpoints/read/observation.py b/api/app/v1/endpoints/read/observation.py index f28aabe1..a5108532 100644 --- a/api/app/v1/endpoints/read/observation.py +++ b/api/app/v1/endpoints/read/observation.py @@ -13,7 +13,9 @@ # limitations under the License. import json +import logging +import asyncpg from app import ANONYMOUS_VIEWER, AUTHORIZATION, REDIS from app.db.asyncpg_db import get_pool from app.db.redis_db import redis @@ -24,6 +26,8 @@ from .query_parameters import CommonQueryParams, get_common_query_params from .read import asyncpg_stream_results, wrapped_result_generator +logger = logging.getLogger(__name__) + v1 = APIRouter() user = Header(default=None, include_in_schema=False) @@ -96,7 +100,7 @@ async def get_observations( media_type="application/json", status_code=status.HTTP_200_OK, ) - except Exception as e: + except StopAsyncIteration: return JSONResponse( status_code=status.HTTP_404_NOT_FOUND, content={ @@ -105,6 +109,29 @@ async def get_observations( "message": "Not Found", }, ) + except ( + asyncpg.PostgresConnectionError, + asyncpg.TooManyConnectionsError, + ): + logger.exception("Database unavailable in get_observations") + return JSONResponse( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + content={ + "code": 503, + "type": "error", + "message": "Database temporarily unavailable", + }, + ) + except Exception: + logger.exception("Unexpected streaming error in get_observations") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={ + "code": 500, + "type": "error", + "message": "Internal server error", + }, + ) except Exception as e: return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, diff --git a/api/app/v1/endpoints/read/observed_property.py b/api/app/v1/endpoints/read/observed_property.py index 5f6ee00e..488f966e 100644 --- a/api/app/v1/endpoints/read/observed_property.py +++ b/api/app/v1/endpoints/read/observed_property.py @@ -13,7 +13,9 @@ # limitations under the License. import json +import logging +import asyncpg from app import ANONYMOUS_VIEWER, AUTHORIZATION, REDIS from app.db.asyncpg_db import get_pool from app.db.redis_db import redis @@ -24,6 +26,8 @@ from .query_parameters import CommonQueryParams, get_common_query_params from .read import asyncpg_stream_results, wrapped_result_generator +logger = logging.getLogger(__name__) + v1 = APIRouter() user = Header(default=None, include_in_schema=False) @@ -96,7 +100,7 @@ async def get_observed_properties( media_type="application/json", status_code=status.HTTP_200_OK, ) - except Exception as e: + except StopAsyncIteration: return JSONResponse( status_code=status.HTTP_404_NOT_FOUND, content={ @@ -105,6 +109,29 @@ async def get_observed_properties( "message": "Not Found", }, ) + except ( + asyncpg.PostgresConnectionError, + asyncpg.TooManyConnectionsError, + ): + logger.exception("Database unavailable in get_observed_properties") + return JSONResponse( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + content={ + "code": 503, + "type": "error", + "message": "Database temporarily unavailable", + }, + ) + except Exception: + logger.exception("Unexpected streaming error in get_observed_properties") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={ + "code": 500, + "type": "error", + "message": "Internal server error", + }, + ) except Exception as e: return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, diff --git a/api/app/v1/endpoints/read/sensor.py b/api/app/v1/endpoints/read/sensor.py index 79639e82..ba018fb7 100644 --- a/api/app/v1/endpoints/read/sensor.py +++ b/api/app/v1/endpoints/read/sensor.py @@ -13,7 +13,9 @@ # limitations under the License. import json +import logging +import asyncpg from app import ANONYMOUS_VIEWER, AUTHORIZATION, REDIS from app.db.asyncpg_db import get_pool from app.db.redis_db import redis @@ -24,6 +26,8 @@ from .query_parameters import CommonQueryParams, get_common_query_params from .read import asyncpg_stream_results, wrapped_result_generator +logger = logging.getLogger(__name__) + v1 = APIRouter() user = Header(default=None, include_in_schema=False) @@ -96,7 +100,7 @@ async def get_sensors( media_type="application/json", status_code=status.HTTP_200_OK, ) - except Exception as e: + except StopAsyncIteration: return JSONResponse( status_code=status.HTTP_404_NOT_FOUND, content={ @@ -105,6 +109,29 @@ async def get_sensors( "message": "Not Found", }, ) + except ( + asyncpg.PostgresConnectionError, + asyncpg.TooManyConnectionsError, + ): + logger.exception("Database unavailable in get_sensors") + return JSONResponse( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + content={ + "code": 503, + "type": "error", + "message": "Database temporarily unavailable", + }, + ) + except Exception: + logger.exception("Unexpected streaming error in get_sensors") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={ + "code": 500, + "type": "error", + "message": "Internal server error", + }, + ) except Exception as e: return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, diff --git a/api/app/v1/endpoints/read/thing.py b/api/app/v1/endpoints/read/thing.py index 29071b47..67b133ba 100644 --- a/api/app/v1/endpoints/read/thing.py +++ b/api/app/v1/endpoints/read/thing.py @@ -13,7 +13,9 @@ # limitations under the License. import json +import logging +import asyncpg from app import ANONYMOUS_VIEWER, AUTHORIZATION, REDIS from app.db.asyncpg_db import get_pool from app.db.redis_db import redis @@ -24,6 +26,8 @@ from .query_parameters import CommonQueryParams, get_common_query_params from .read import asyncpg_stream_results, wrapped_result_generator +logger = logging.getLogger(__name__) + v1 = APIRouter() user = Header(default=None, include_in_schema=False) @@ -96,7 +100,7 @@ async def get_things( media_type="application/json", status_code=status.HTTP_200_OK, ) - except Exception as e: + except StopAsyncIteration: return JSONResponse( status_code=status.HTTP_404_NOT_FOUND, content={ @@ -105,6 +109,29 @@ async def get_things( "message": "Not Found", }, ) + except ( + asyncpg.PostgresConnectionError, + asyncpg.TooManyConnectionsError, + ): + logger.exception("Database unavailable in get_things") + return JSONResponse( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + content={ + "code": 503, + "type": "error", + "message": "Database temporarily unavailable", + }, + ) + except Exception: + logger.exception("Unexpected streaming error in get_things") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={ + "code": 500, + "type": "error", + "message": "Internal server error", + }, + ) except Exception as e: return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST,