Skip to main content

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

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mobius_error_py-1.0.0.tar.gz (27.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mobius_error_py-1.0.0-py3-none-any.whl (27.9 kB view details)

Uploaded Python 3

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

Hashes for mobius_error_py-1.0.0.tar.gz
Algorithm Hash digest
SHA256 bcb38bd69c9c6067b04be729ea77589952d59cff4c9e8e4c13eb4eecdb3b7a85
MD5 0e6b9171a0b1a77ffd9f1ca276a45ccd
BLAKE2b-256 d86477accbb35cbaf19c7ad4e6b84eceaf1cbddd67c663e4501731d03d6b792c

See more details on using hashes here.

File details

Details for the file mobius_error_py-1.0.0-py3-none-any.whl.

File metadata

File hashes

Hashes for mobius_error_py-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f9e398d5e354693fadf13712a1cfb0c0203a4705d4d38175a70e3807083e98f0
MD5 8ed0f86f854c5212a1f6bd16faae55de
BLAKE2b-256 d14e6dcef51a69632a19dc5451ba6c88daf6540a10705e928d098921c9feab5d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page