-
Notifications
You must be signed in to change notification settings - Fork 16
fix(create,update): standardize duplicate key error handling across endpoints #64
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,8 +17,11 @@ | |
| from urllib.parse import parse_qs, quote, urlencode, urlparse, urlunparse | ||
|
|
||
| from app import EPSG, HOSTNAME, TOP_VALUE | ||
| from asyncpg.exceptions import UniqueViolationError | ||
| from asyncpg.types import Range | ||
| from dateutil import parser | ||
| from fastapi import status | ||
| from fastapi.responses import JSONResponse | ||
|
|
||
| _USERNAME_RE = re.compile(r"^[a-zA-Z0-9_]{3,63}$") | ||
|
|
||
|
|
@@ -269,6 +272,29 @@ def validate_required_keys(payload, required_keys): | |
| raise Exception(f"Missing required fields: {', '.join(missing)}") | ||
|
|
||
|
|
||
| def handle_duplicate_error(e=None): | ||
| message = "Entity already exists." | ||
|
|
||
| if e and hasattr(e, "detail") and e.detail: | ||
| # Example detail: "Key (name)=(duplicate test) already exists." | ||
| # We can extract the column name and the value from here | ||
| import re | ||
| match = re.search(r"Key \((.*?)\)=\((.*?)\) already exists", e.detail) | ||
| if match: | ||
| column = match.group(1) | ||
| value = match.group(2) | ||
| message = f"An entity with {column} '{value}' already exists." | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Im a bit unsure about parsing |
||
|
|
||
| return JSONResponse( | ||
| status_code=status.HTTP_409_CONFLICT, | ||
| content={ | ||
| "code": 409, | ||
| "type": "error", | ||
| "message": message, | ||
| }, | ||
| ) | ||
|
|
||
|
|
||
| def validate_epsg(key): | ||
| crs = key.get("crs") | ||
| if crs is not None: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,9 +14,13 @@ | |
|
|
||
| from app import AUTHORIZATION, POSTGRES_PORT_WRITE, VERSIONING | ||
| from app.db.asyncpg_db import get_pool, get_pool_w | ||
| from app.utils.utils import validate_payload_keys, validate_required_keys | ||
| from app.utils.utils import ( | ||
| handle_duplicate_error, | ||
| validate_payload_keys, | ||
| validate_required_keys, | ||
| ) | ||
| from app.v1.endpoints.functions import set_role | ||
| from asyncpg.exceptions import InsufficientPrivilegeError | ||
| from asyncpg.exceptions import InsufficientPrivilegeError, UniqueViolationError | ||
| from fastapi import APIRouter, Body, Depends, Header, Request, status | ||
| from fastapi.responses import JSONResponse, Response | ||
|
|
||
|
|
@@ -81,6 +85,7 @@ async def create_location( | |
|
|
||
| async with pool.acquire() as connection: | ||
| async with connection.transaction(): | ||
|
|
||
| if current_user is not None: | ||
| await set_role(connection, current_user) | ||
|
|
||
|
|
@@ -100,6 +105,8 @@ async def create_location( | |
| status_code=status.HTTP_201_CREATED, | ||
| headers={"location": header}, | ||
| ) | ||
| except UniqueViolationError as e: | ||
| return handle_duplicate_error(e) | ||
| except InsufficientPrivilegeError: | ||
| return JSONResponse( | ||
| status_code=status.HTTP_401_UNAUTHORIZED, | ||
|
|
@@ -115,7 +122,7 @@ async def create_location( | |
| content={ | ||
| "code": 400, | ||
| "type": "error", | ||
| "message": str(e), | ||
| "message": f"[{type(e).__name__}] {str(e)}" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don’t think we should include type(e).name or raw str(e) in the client response here. That still exposes internal implementation details and can leak database/backend information. For this case, it would be safer to return a sanitized conflict message and keep the original exception only in server-side logs |
||
| }, | ||
| ) | ||
|
|
||
|
|
@@ -172,6 +179,8 @@ async def create_location_for_thing( | |
| status_code=status.HTTP_201_CREATED, | ||
| headers={"location": header}, | ||
| ) | ||
| except UniqueViolationError as e: | ||
| return handle_duplicate_error(e) | ||
| except InsufficientPrivilegeError: | ||
| return JSONResponse( | ||
| status_code=status.HTTP_401_UNAUTHORIZED, | ||
|
|
@@ -187,6 +196,6 @@ async def create_location_for_thing( | |
| content={ | ||
| "code": 400, | ||
| "type": "error", | ||
| "message": str(e), | ||
| "message": f"[{type(e).__name__}] {str(e)}", | ||
| }, | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,13 +14,13 @@ | |
|
|
||
| from app import AUTHORIZATION, POSTGRES_PORT_WRITE, VERSIONING | ||
| from app.db.asyncpg_db import get_pool, get_pool_w | ||
| from app.utils.utils import validate_payload_keys | ||
| from app.utils.utils import handle_duplicate_error, validate_payload_keys | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could you import |
||
| from app.v1.endpoints.functions import ( | ||
| get_datastreams_from_foi, | ||
| set_role, | ||
| update_datastream_observedArea, | ||
| ) | ||
| from asyncpg.exceptions import InsufficientPrivilegeError | ||
| from asyncpg.exceptions import InsufficientPrivilegeError, UniqueViolationError | ||
| from fastapi import APIRouter, Body, Depends, Header, status | ||
| from fastapi.responses import JSONResponse, Response | ||
|
|
||
|
|
@@ -133,6 +133,8 @@ async def update_feature_of_interest( | |
| await connection.execute("RESET ROLE;") | ||
|
|
||
| return Response(status_code=status.HTTP_200_OK) | ||
| except UniqueViolationError as e: | ||
| return handle_duplicate_error(e) | ||
| except InsufficientPrivilegeError: | ||
| return JSONResponse( | ||
| status_code=status.HTTP_401_UNAUTHORIZED, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why do we need to import 'UniqueViolationError' if we are not using it