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-1.0.0.tar.gz (25.3 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-1.0.0-py3-none-any.whl (24.7 kB view details)

Uploaded Python 3

File details

Details for the file mobius_error-1.0.0.tar.gz.

File metadata

  • Download URL: mobius_error-1.0.0.tar.gz
  • Upload date:
  • Size: 25.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for mobius_error-1.0.0.tar.gz
Algorithm Hash digest
SHA256 d70970edc516b3292ae0e4cca3d51ba4d34c5c7a1bba9dede9c3235c8f4cca37
MD5 86ba016a76e4e994f1a6129fe7ff46f5
BLAKE2b-256 7e42a8c73c85049139c8102cbef6fb478f08cf53340888cc587cf2179a2d7d6a

See more details on using hashes here.

File details

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

File metadata

  • Download URL: mobius_error-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 24.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for mobius_error-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3c39bc965e6158939dc2ff6a02c7566053db723128a76873eb889ddd88887b14
MD5 36b346f0d687fa3a01a14a6d45090ef9
BLAKE2b-256 baf56dcf9214b1e5f401279d171bacebc855b30e12dfee53998cd50f0c5fdb42

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