Mobius Error Handling Library for FastAPI — structured error responses ported from Spring Boot
Project description
Mobius Error Services for FastAPI
A comprehensive error handling library for FastAPI, ported from the Spring Boot error-spring-lib. It provides a structured, consistent error response format across all your microservices — matching the exact JSON contract your existing Java services already produce.
Table of Contents
- Installation
- Quick Start
- Response Format
- Exception Hierarchy
- Built-in Error Codes
- Usage Examples
- Exception Types Reference
- Advanced Usage
- Java ↔ Python Mapping
- Project Structure
Installation
Copy the mobius_error/ package into your project, or install it as an editable package:
cd error_lib
pip install -e .
Requirements: Python 3.10+, FastAPI >= 0.100.0
Quick Start
Two lines to integrate into any FastAPI app:
from fastapi import FastAPI
from mobius_error import register_exception_handlers
app = FastAPI(title="my-service")
register_exception_handlers(app, app_name="my-service")
That's it. Every unhandled exception now returns a structured JSON response instead of a raw 500.
Response Format
All error responses follow this consistent JSON structure (empty fields are omitted):
{
"timestamp": 1706000000000,
"origin": "my-service",
"httpStatusCode": 404,
"errorCode": 404001,
"errorMessage": "User not found",
"detailedErrorMessage": "No user with ID 42 exists in the database",
"subErrors": [
{
"message": "Checked primary and replica databases",
"timestamp": 1706000000
}
],
"actionsRequired": [
"Verify the user ID and try again"
],
"cause": {
"message": "Record not found in table 'users'",
"origin": "my-service",
"timestamp": 1706000000
},
"docUrl": "https://mobiusdtaas.atlassian.net/wiki/spaces/EN/pages/2360868868/..."
}
| Field | Type | Description |
|---|---|---|
timestamp |
int |
Unix timestamp in milliseconds |
origin |
string |
Name of the service that produced the error |
httpStatusCode |
int |
HTTP status code |
errorCode |
int |
Application-specific numeric error code |
errorMessage |
string |
Human-readable summary |
detailedErrorMessage |
string |
More context about what went wrong |
subErrors |
ErrorMessage[] |
List of nested/related error details |
actionsRequired |
string[] |
Suggested actions the caller can take |
cause |
ErrorMessage |
Root cause information (e.g., from a wrapped exception) |
docUrl |
string |
Link to documentation for common issues |
Exception Hierarchy
The exception class hierarchy mirrors the Java library exactly:
Exception
└── ApplicationException (base — caught as 403)
└── ApiException (uses Error's HTTP status)
├── ValidationException (caught as 409)
│ ├── AccessViolationException (caught as 403)
│ └── InvalidNameException
├── TokenException (uses Error's HTTP status)
├── DataTypeMismatchException (caught as 400)
└── MethodArgumentsNotValidException
├── RestException
├── RestGetException
├── RestPostException
├── KafkaException
├── KafkaConsumptionException
├── ObjectMappingException
├── InvalidTenantException
├── UnsupportedOperationException
└── GroupDataRetrievalException
Each exception handler matches the behavior of its Java @ExceptionHandler counterpart.
Built-in Error Codes
These are defined in CommonErrors and match the Java CommonErrors class:
| Constant | HTTP | Code | Message |
|---|---|---|---|
INVALID_TENANT_ID |
403 | 403002 | You're not registered as a tenant yet |
TENANT_NOT_AUTHORIZED |
401 | 401000 | You're not authorized to access the resource |
UNEXPECTED_ERROR |
500 | 500000 | Encountered an unexpected error |
SERVICE_UNAVAILABLE |
500 | 500001 | One or more service(s) are not up and running |
OBJECT_MAPPING_FAILURE |
500 | 500002 | Failed to convert json to object/map |
GROUP_DATA_RETRIEVAL_FAILURE |
500 | 500003 | Failed to get group data from data lake |
GET_API_FAILURE |
500 | 500004 | Failed to GET data from the URL |
POST_API_FAILURE |
500 | 500005 | Failed to make POST call to an api |
REST_API_FAILURE |
500 | 500006 | Failed to make REST call to an api |
KAFKA_ERROR |
400 | 400000 | An error occurred in kafka |
KAFKA_CONSUMPTION_ERROR |
400 | 400001 | An error occurred while consuming from kafka |
INVALID_NAME |
400 | 400002 | The name provided is invalid |
UNSUPPORTED_OPERATION |
400 | 400003 | This operation is not yet supported |
Usage Examples
Basic: Raising Exceptions
from mobius_error import ApplicationException, ApiException, CommonErrors
# Simple — uses built-in error definition
@app.get("/items/{item_id}")
async def get_item(item_id: int):
item = db.find(item_id)
if not item:
raise ApiException(
error=CommonErrors.GET_API_FAILURE,
detailed_error_message=f"Item {item_id} not found in database",
)
return item
Custom Error Codes
Define your own errors following the same pattern as CommonErrors:
from mobius_error import Error, ApiException
class MyErrors:
USER_NOT_FOUND = Error(404, 404001, "User not found", "Check the user ID and try again")
DUPLICATE_EMAIL = Error(409, 409001, "Email already exists", "Use a different email address")
QUOTA_EXCEEDED = Error(429, 429001, "Rate limit exceeded", "Wait a moment and retry")
@app.get("/users/{user_id}")
async def get_user(user_id: int):
user = await find_user(user_id)
if not user:
raise ApiException(
error=MyErrors.USER_NOT_FOUND,
detailed_error_message=f"No user with ID {user_id}",
)
return user
Fluent Builder Pattern
Chain .add_sub_error() and .add_action_required() for rich error detail:
from mobius_error import ApplicationException, CommonErrors
@app.post("/deploy")
async def deploy():
errors = run_preflight_checks()
if errors:
raise (
ApplicationException(
error=CommonErrors.UNEXPECTED_ERROR,
detailed_error_message="Deployment preflight checks failed",
)
.add_sub_error("Health check: database unreachable")
.add_sub_error("Health check: cache latency > 500ms")
.add_action_required("Verify database connectivity")
.add_action_required("Check Redis cluster status")
)
This produces a response with all sub-errors and actions listed:
{
"errorCode": 500000,
"errorMessage": "Encountered an unexpected error",
"detailedErrorMessage": "Deployment preflight checks failed",
"subErrors": [
{ "message": "Health check: database unreachable" },
{ "message": "Health check: cache latency > 500ms" }
],
"actionsRequired": [
"Kindly try again after some time or contact the support team",
"Verify database connectivity",
"Check Redis cluster status"
]
}
Wrapping Upstream Failures
Preserve the original cause when catching exceptions from external calls:
import httpx
from mobius_error import RestGetException
@app.get("/proxy/weather")
async def proxy_weather():
try:
async with httpx.AsyncClient() as client:
resp = await client.get("https://api.weather.com/forecast")
resp.raise_for_status()
return resp.json()
except httpx.HTTPStatusError as exc:
raise RestGetException(
error_message="Failed to fetch weather forecast",
url="https://api.weather.com/forecast",
status_code=exc.response.status_code,
response_body=exc.response.text,
cause=exc,
)
Pydantic Validation (Automatic)
FastAPI/Pydantic validation errors are caught automatically and formatted as sub-errors:
from pydantic import BaseModel, Field
class CreateUserRequest(BaseModel):
name: str = Field(..., min_length=2, max_length=50)
email: str = Field(..., pattern=r"^[\w.-]+@[\w.-]+\.\w+$")
age: int = Field(..., ge=0, le=150)
@app.post("/users")
async def create_user(body: CreateUserRequest):
# If validation fails, the library auto-returns a 422 with sub-errors:
# {
# "errorCode": 400000,
# "errorMessage": "Validation error",
# "subErrors": [
# { "message": "body → name: String should have at least 2 characters" },
# { "message": "body → email: String should match pattern ..." }
# ]
# }
return {"created": body.model_dump()}
Exception Types Reference
ApplicationException → HTTP 403
The base exception. Use when something unexpected goes wrong.
raise ApplicationException(
error=CommonErrors.UNEXPECTED_ERROR, # required
detailed_error_message="What happened", # optional
error_object={"debug": "info"}, # optional, included in response
cause=original_exception, # optional, populates 'cause' field
)
ApiException → Uses Error's HTTP status
Most common for API-level errors. The HTTP status comes from the Error object.
raise ApiException(
error=MyErrors.USER_NOT_FOUND, # Error with http_status_code=404
detailed_error_message="User 42 not found",
)
# → Returns HTTP 404
ValidationException → HTTP 409
For data validation failures caught in business logic (not Pydantic).
raise ValidationException(
error=MyErrors.DUPLICATE_EMAIL,
detailed_error_message="user@example.com already registered",
)
AccessViolationException → HTTP 403
For authorization/permission failures.
raise AccessViolationException(
error=CommonErrors.TENANT_NOT_AUTHORIZED,
detailed_error_message="Tenant xyz cannot access project abc",
)
TokenException → Uses Error's HTTP status
For authentication/token failures.
raise TokenException(
error=CommonErrors.TENANT_NOT_AUTHORIZED,
detailed_error_message="JWT token expired",
)
# → Returns HTTP 401
DataTypeMismatchException → HTTP 400
For type conversion failures.
raise DataTypeMismatchException(
error=Error(400, 400004, "Invalid input type", "Provide valid input"),
detailed_error_message="Expected int for 'age', got string",
error_object={"field": "age", "received": "twenty-five"},
)
InvalidNameException → HTTP 409
Shortcut for invalid name validation.
raise InvalidNameException("Name contains invalid characters: @#$")
InvalidTenantException → HTTP 409
Shortcut for invalid tenant validation.
raise InvalidTenantException("Tenant ID 'null' is not valid")
ObjectMappingException → HTTP 403
For serialization/deserialization failures.
raise ObjectMappingException("Failed to parse user payload", cause=json_error)
RestGetException / RestPostException / RestException → HTTP 403
For upstream HTTP call failures.
raise RestGetException(
error_message="Upstream service unavailable",
url="https://api.example.com/data",
status_code=502,
response_body='{"error": "bad gateway"}',
)
KafkaException / KafkaConsumptionException → HTTP 403
For message queue failures.
raise KafkaException("Failed to publish event to topic 'orders'")
raise KafkaConsumptionException("Failed to deserialize message", cause=exc)
UnsupportedOperationException → HTTP 403
For operations not yet implemented.
raise UnsupportedOperationException("PATCH is not supported on this resource")
Advanced Usage
Setting the Application Name
The origin field in every error response is set from the app name. You can configure it in three ways:
# Option 1: Pass explicitly (recommended)
register_exception_handlers(app, app_name="my-service")
# Option 2: Falls back to app.title
app = FastAPI(title="my-service")
register_exception_handlers(app)
# Option 3: Set manually at any time
from mobius_error import App
App.set_app_name("my-service")
Custom Exception Classes
Extend the hierarchy for your domain:
from mobius_error import ApiException, Error
class PaymentErrors:
INSUFFICIENT_FUNDS = Error(402, 402001, "Insufficient funds", "Add funds to your account")
CARD_DECLINED = Error(402, 402002, "Card declined", "Try a different payment method")
class PaymentException(ApiException):
def __init__(self, error: Error, detail: str, transaction_id: str | None = None):
super().__init__(error=error, detailed_error_message=detail)
if transaction_id:
self.add_sub_error(f"Transaction ID: {transaction_id}")
# Register handler for your custom exception
@app.exception_handler(PaymentException)
async def handle_payment(request, exc):
from mobius_error.responses.api_error_response import ApiErrorResponse
from fastapi.responses import JSONResponse
resp = ApiErrorResponse.from_application_exception(exc.http_status_code, exc)
return JSONResponse(status_code=exc.http_status_code, content=resp.to_dict())
# Usage
raise PaymentException(
PaymentErrors.CARD_DECLINED,
detail="Visa ending in 4242 was declined",
transaction_id="txn_abc123",
)
Combining with FastAPI's HTTPException
The library catches Starlette/FastAPI HTTPException too, wrapping them in the Mobius format. So raise HTTPException(status_code=404, detail="Not found") will return:
{
"httpStatusCode": 404,
"errorCode": 500001,
"errorMessage": "Not found",
"origin": "my-service"
}
Java ↔ Python Mapping
| Java (Spring Boot) | Python (FastAPI) |
|---|---|
@ControllerAdvice |
register_exception_handlers(app) |
@ExceptionHandler(ApiException.class) |
@app.exception_handler(ApiException) |
new Error(HttpStatus.NOT_FOUND, ...) |
Error(404, 404001, ...) |
throw new ApiException(error, msg) |
raise ApiException(error=error, detailed_error_message=msg) |
new ApiErrorResponse(status, msg) |
ApiErrorResponse.from_status(status, msg) |
ApplicationException.addSubError(msg) |
.add_sub_error(msg) |
App.setAppName(name) |
App.set_app_name(name) |
@Value("${spring.application.name}") |
register_exception_handlers(app, app_name=...) |
ResponseEntity<ApiErrorResponse> |
JSONResponse(status_code=..., content=resp.to_dict()) |
Project Structure
error_lib/
├── pyproject.toml # Package definition
├── example_app.py # Full working demo (run with uvicorn)
├── test_smoke.py # Smoke tests for all endpoints
└── mobius_error/
├── __init__.py # Public API — import everything from here
├── config.py # App.get_app_name() / App.set_app_name()
├── errors/
│ ├── error.py # Error dataclass (status + code + message)
│ ├── error_message.py # ErrorMessage model (nested cause chain)
│ └── common_errors.py # Built-in error constants (CommonErrors)
├── exceptions/
│ ├── application_exception.py # Base exception
│ ├── api_exception.py # API-level exception
│ ├── validation_exception.py # Validation errors
│ ├── access_violation_exception.py
│ ├── token_exception.py
│ ├── data_type_mismatch_exception.py
│ ├── rest_exception.py
│ ├── rest_get_exception.py
│ ├── rest_post_exception.py
│ ├── kafka_exception.py
│ ├── kafka_consumption_exception.py
│ ├── object_mapping_exception.py
│ ├── invalid_name_exception.py
│ ├── invalid_tenant_exception.py
│ ├── unsupported_operation_exception.py
│ └── group_data_retrieval_exception.py
├── handlers/
│ └── exception_handler.py # All FastAPI exception handlers + middleware
└── responses/
├── error_response.py # Base response model
└── api_error_response.py # Full API response with factory methods
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mobius_error_py-1.0.0.tar.gz.
File metadata
- Download URL: mobius_error_py-1.0.0.tar.gz
- Upload date:
- Size: 27.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bcb38bd69c9c6067b04be729ea77589952d59cff4c9e8e4c13eb4eecdb3b7a85
|
|
| MD5 |
0e6b9171a0b1a77ffd9f1ca276a45ccd
|
|
| BLAKE2b-256 |
d86477accbb35cbaf19c7ad4e6b84eceaf1cbddd67c663e4501731d03d6b792c
|
File details
Details for the file mobius_error_py-1.0.0-py3-none-any.whl.
File metadata
- Download URL: mobius_error_py-1.0.0-py3-none-any.whl
- Upload date:
- Size: 27.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f9e398d5e354693fadf13712a1cfb0c0203a4705d4d38175a70e3807083e98f0
|
|
| MD5 |
8ed0f86f854c5212a1f6bd16faae55de
|
|
| BLAKE2b-256 |
d14e6dcef51a69632a19dc5451ba6c88daf6540a10705e928d098921c9feab5d
|