error-handling

📁 youngger9765/career_ios_backend 📅 1 day ago
0
总安装量
1
周安装量
安装命令
npx skills add https://github.com/youngger9765/career_ios_backend --skill error-handling

Agent 安装分布

amp 1
cline 1
opencode 1
cursor 1
continue 1
kimi-cli 1

Skill 文档

Error Handling Skill

Purpose

Consistent error handling patterns for career_ios_backend FastAPI application.

Auto-Activation

Triggers on:

  • ✅ “error”, “exception”, “validation”
  • ✅ “錯誤處理”, “異常處理”
  • ✅ “error handling”, “exception handling”

Core Principles (Prototype Phase)

Keep it simple:

  • ✅ Use FastAPI’s HTTPException
  • ✅ Return clear error messages
  • ✅ Log errors appropriately
  • ❌ Don’t over-engineer custom exceptions (yet)

Standard Error Response Format

FastAPI HTTPException Pattern

from fastapi import HTTPException, status

# 404 Not Found
raise HTTPException(
    status_code=status.HTTP_404_NOT_FOUND,
    detail="Client not found"
)

# 400 Bad Request
raise HTTPException(
    status_code=status.HTTP_400_BAD_REQUEST,
    detail="Invalid client code format"
)

# 401 Unauthorized
raise HTTPException(
    status_code=status.HTTP_401_UNAUTHORIZED,
    detail="Invalid credentials",
    headers={"WWW-Authenticate": "Bearer"}
)

# 403 Forbidden
raise HTTPException(
    status_code=status.HTTP_403_FORBIDDEN,
    detail="Insufficient permissions"
)

# 500 Internal Server Error
raise HTTPException(
    status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
    detail="Database connection failed"
)

HTTP Status Codes (Quick Reference)

Code When to Use Example
200 Success GET resource
201 Created POST new resource
204 No Content DELETE successful
400 Bad Request Invalid input
401 Unauthorized Missing/invalid token
403 Forbidden Insufficient permissions
404 Not Found Resource doesn’t exist
409 Conflict Duplicate resource
422 Validation Error Pydantic validation fails
500 Server Error Unexpected error

Common Patterns

Pattern 1: Resource Not Found

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session

@router.get("/clients/{client_id}")
async def get_client(
    client_id: int,
    db: Session = Depends(get_db)
):
    client = db.query(Client).filter(Client.id == client_id).first()

    if not client:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Client with id {client_id} not found"
        )

    return client

Pattern 2: Validation Error

@router.post("/clients", status_code=status.HTTP_201_CREATED)
async def create_client(
    client_data: ClientCreate,
    db: Session = Depends(get_db)
):
    # Check for duplicate
    existing = db.query(Client).filter(
        Client.email == client_data.email
    ).first()

    if existing:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Client with email {client_data.email} already exists"
        )

    # Create client
    new_client = Client(**client_data.dict())
    db.add(new_client)
    db.commit()
    db.refresh(new_client)

    return new_client

Pattern 3: Database Error

from sqlalchemy.exc import SQLAlchemyError
import logging

logger = logging.getLogger(__name__)

@router.post("/clients")
async def create_client(
    client_data: ClientCreate,
    db: Session = Depends(get_db)
):
    try:
        new_client = Client(**client_data.dict())
        db.add(new_client)
        db.commit()
        db.refresh(new_client)
        return new_client

    except SQLAlchemyError as e:
        db.rollback()
        logger.error(f"Database error creating client: {str(e)}", exc_info=True)
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="Failed to create client"
        )

Pattern 4: Authentication Error

from app.core.security import verify_password

@router.post("/auth/login")
async def login(
    credentials: LoginRequest,
    db: Session = Depends(get_db)
):
    user = db.query(User).filter(User.username == credentials.username).first()

    if not user or not verify_password(credentials.password, user.hashed_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"}
        )

    # Generate token
    access_token = create_access_token(data={"sub": user.username})
    return {"access_token": access_token, "token_type": "bearer"}

Pydantic Validation (Automatic)

FastAPI automatically validates request bodies using Pydantic:

from pydantic import BaseModel, EmailStr, validator

class ClientCreate(BaseModel):
    name: str
    email: EmailStr  # Automatic email validation
    age: int

    @validator('age')
    def age_must_be_positive(cls, v):
        if v < 0:
            raise ValueError('Age must be positive')
        return v

    @validator('name')
    def name_must_not_be_empty(cls, v):
        if not v.strip():
            raise ValueError('Name cannot be empty')
        return v

Automatic 422 Response: When validation fails, FastAPI returns:

{
  "detail": [
    {
      "loc": ["body", "email"],
      "msg": "value is not a valid email address",
      "type": "value_error.email"
    }
  ]
}

Logging Best Practices

Basic Logging Setup

import logging

# At the top of your module
logger = logging.getLogger(__name__)

# Log levels
logger.debug("Detailed debug info")      # Development only
logger.info("General information")       # Normal operations
logger.warning("Warning message")        # Potential issues
logger.error("Error occurred")           # Error happened
logger.critical("Critical failure")      # System failure

Log with Context

@router.post("/clients")
async def create_client(client_data: ClientCreate, db: Session = Depends(get_db)):
    logger.info(f"Creating client: {client_data.name}")

    try:
        # ... create client
        logger.info(f"Client created successfully: id={new_client.id}")
        return new_client

    except Exception as e:
        logger.error(
            f"Failed to create client: {client_data.name}",
            exc_info=True  # Include full traceback
        )
        raise

What to Log

✅ Do Log:

  • Incoming requests (at INFO level)
  • Successful operations (at INFO level)
  • Errors with context (at ERROR level)
  • Authentication failures (at WARNING level)

❌ Don’t Log:

  • Passwords or tokens
  • Sensitive user data
  • Too much detail in production (use DEBUG level in development)

Error Handling Checklist

Before Commit

  • All error cases return appropriate HTTP status codes
  • Error messages are clear and actionable
  • Sensitive data not exposed in error messages
  • Errors are logged with sufficient context
  • Database transactions rolled back on error
  • Tests cover error scenarios

Testing Error Cases

import pytest
from httpx import AsyncClient
from app.main import app

@pytest.mark.asyncio
async def test_client_not_found():
    """Test 404 when client doesn't exist"""
    async with AsyncClient(app=app, base_url="http://test") as client:
        response = await client.get("/api/v1/clients/99999")

    assert response.status_code == 404
    assert "not found" in response.json()["detail"].lower()

@pytest.mark.asyncio
async def test_duplicate_client(auth_headers):
    """Test 400 when creating duplicate client"""
    client_data = {
        "name": "Test Client",
        "email": "test@example.com"
    }

    async with AsyncClient(app=app, base_url="http://test") as client:
        # Create first time
        response1 = await client.post(
            "/api/v1/clients",
            headers=auth_headers,
            json=client_data
        )
        assert response1.status_code == 201

        # Try to create duplicate
        response2 = await client.post(
            "/api/v1/clients",
            headers=auth_headers,
            json=client_data
        )
        assert response2.status_code == 400
        assert "already exists" in response2.json()["detail"].lower()

@pytest.mark.asyncio
async def test_invalid_token():
    """Test 401 with invalid token"""
    async with AsyncClient(app=app, base_url="http://test") as client:
        response = await client.get(
            "/api/v1/clients",
            headers={"Authorization": "Bearer invalid_token"}
        )

    assert response.status_code == 401

Quick Reference Table

Scenario Status Code Pattern
Resource not found 404 Check if not resource: raise HTTPException(404)
Invalid input 400 Validate before processing
Duplicate resource 400 Check existence first
Unauthorized 401 Verify token/credentials
Forbidden 403 Check permissions
Validation error 422 Use Pydantic validators
Database error 500 Try-except with rollback

Anti-Patterns to Avoid

❌ Generic Error Messages

# Bad
raise HTTPException(status_code=400, detail="Bad request")

# Good
raise HTTPException(
    status_code=400,
    detail="Client email format is invalid"
)

❌ Exposing Internal Details

# Bad - exposes database structure
raise HTTPException(
    status_code=500,
    detail=f"SQLAlchemy error: {str(e)}"
)

# Good - user-friendly message
logger.error(f"Database error: {str(e)}", exc_info=True)
raise HTTPException(
    status_code=500,
    detail="Failed to process request"
)

❌ Swallowing Exceptions

# Bad - silently fails
try:
    db.commit()
except Exception:
    pass  # Error ignored!

# Good - handle or re-raise
try:
    db.commit()
except Exception as e:
    logger.error(f"Commit failed: {e}", exc_info=True)
    db.rollback()
    raise HTTPException(500, detail="Operation failed")

Related Skills

  • api-development: API design patterns
  • debugging: Debug error scenarios
  • quality-standards: Code quality requirements

Skill Version: v1.0 Last Updated: 2025-12-25 Project: career_ios_backend (Prototype Phase)