This document describes the comprehensive error handling and validation system implemented for the transcript-create application. The system provides consistent, user-friendly error messages while maintaining security by not exposing internal implementation details.
All custom exceptions inherit from AppError (formerly AppException), which provides:
- Consistent error format: Every error has a
error_code,message,status_code, and optionaldetails - JSON serialization:
to_dict()method for easy API responses - HTTP status code mapping: Each exception knows its appropriate HTTP status code
| Exception | Status Code | Usage |
|---|---|---|
JobNotFoundError |
404 | When a job cannot be found by ID |
VideoNotFoundError |
404 | When a video cannot be found by ID |
InvalidURLError |
400 | When a URL is invalid or not supported |
QuotaExceededError |
402 | When user exceeds their quota |
TranscriptNotReadyError |
409 | When transcript is requested but not ready |
DatabaseError |
500 | When database operations fail |
ExternalServiceError |
503 | When external services (Stripe, OAuth, etc.) fail |
AuthenticationError |
401 | When authentication is required but not provided |
AuthorizationError |
403 | When user lacks required permissions |
ValidationError |
422 | When input validation fails |
RateLimitError |
429 | When rate limits are exceeded |
DuplicateJobError |
409 | When attempting to create duplicate jobs |
Centralized exception handlers convert exceptions to consistent JSON responses:
- AppError Handler: Handles all custom application exceptions
- RequestValidationError Handler: Handles Pydantic validation errors
- SQLAlchemy Handler: Handles database errors without exposing SQL details
- General Exception Handler: Catches all unhandled exceptions
All errors follow a consistent JSON structure:
{
"error": "error_code",
"message": "Human-readable error message",
"details": {
"field": "optional_field_name",
"additional": "context"
}
}Every request gets a unique X-Request-ID header for tracing:
@app.middleware("http")
async def add_request_id(request: Request, call_next):
request_id = str(uuid.uuid4())
request.state.request_id = request_id
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return responseEnhanced with validators:
- URL validation: Ensures URLs are valid HTTP/HTTPS URLs
- YouTube URL validation: Custom validator ensures only YouTube URLs are accepted
- Kind validation: Uses
Literal["single", "channel"]for type safety
class JobCreate(BaseModel):
url: HttpUrl
kind: Literal["single", "channel"] = "single"
@field_validator("url")
@classmethod
def validate_youtube_url(cls, v: HttpUrl) -> HttpUrl:
# Validates against YouTube URL patternsValidates search parameters:
class SearchQuery(BaseModel):
q: str = Field(..., min_length=1, max_length=500)
source: Literal["native", "youtube"] = "native"
video_id: Optional[uuid.UUID] = None
limit: int = Field(50, ge=1, le=200)
offset: int = Field(0, ge=0)All database operations are wrapped with a retry decorator that:
- Detects transient errors (connection issues, timeouts, deadlocks)
- Retries up to 3 times with exponential backoff
- Logs retry attempts for debugging
- Re-raises non-transient errors immediately
@_retry_on_transient_error
def create_job(db, kind: str, url: str):
# Database operation- SQL errors are caught by the global exception handler
- Internal details are logged server-side
- Users receive generic "database error" messages
- Connection errors return 503 (Service Unavailable)
All route files have been updated to use custom exceptions:
app/routes/jobs.py: UsesJobNotFoundErrorapp/routes/videos.py: UsesVideoNotFoundError,TranscriptNotReadyErrorapp/routes/search.py: UsesValidationError,QuotaExceededError,ExternalServiceErrorapp/routes/billing.py: UsesAuthenticationError,ExternalServiceError,ValidationErrorapp/routes/auth.py: UsesExternalServiceError,ValidationErrorapp/routes/favorites.py: UsesAuthenticationError,ValidationErrorapp/routes/exports.py: UsesTranscriptNotReadyErrorapp/routes/admin.py: UsesAuthorizationError,ValidationError
All errors are logged with context:
logger.warning(
"Application error: %s | path=%s request_id=%s details=%s",
exc.message,
request.url.path,
request_id,
exc.details,
)- SQL details are not exposed to users
- Authentication tokens are not logged
- Personal information is sanitized
- Tests all exception types
- Verifies error codes and status codes
- Tests serialization with
to_dict() - Validates detail fields
- Tests error response format across all endpoints
- Validates HTTP status codes
- Tests request ID tracking
- Tests YouTube URL validation
- Tests transcript error scenarios
# Run all exception tests
pytest tests/test_exceptions.py -v
# Run error handling integration tests
pytest tests/test_error_handling.py -v
# Run schema validation tests
pytest tests/test_schemas.py -vfrom app.exceptions import JobNotFoundError, ValidationError
# In route handlers
@router.get("/jobs/{job_id}")
def get_job(job_id: uuid.UUID, db=Depends(get_db)):
job = crud.fetch_job(db, job_id)
if not job:
raise JobNotFoundError(str(job_id))
return jobfrom app.exceptions import ExternalServiceError
try:
response = requests.post(external_api_url, json=data)
response.raise_for_status()
except requests.exceptions.RequestException as e:
raise ExternalServiceError("External API", str(e))from app.exceptions import ValidationError
if not payload.get("required_field"):
raise ValidationError(
"Required field is missing",
field="required_field"
)| Code | Description | Use Case |
|---|---|---|
| 400 | Bad Request | Invalid input, malformed data |
| 401 | Unauthorized | Authentication required |
| 402 | Payment Required | Quota exceeded, upgrade needed |
| 403 | Forbidden | Insufficient permissions |
| 404 | Not Found | Resource not found |
| 409 | Conflict | Duplicate resource, state conflict |
| 422 | Unprocessable Entity | Validation errors |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Unexpected server errors |
| 503 | Service Unavailable | Database/external service unavailable |
- Always use custom exceptions instead of
HTTPException - Provide user-friendly error messages
- Include relevant context in
detailsfield - Log errors with request IDs for tracing
- Use specific exception types for different scenarios
- Don't expose internal implementation details (SQL, stack traces)
- Don't log sensitive information (passwords, tokens)
- Don't use generic error messages without context
- Don't raise
HTTPExceptiondirectly (use custom exceptions) - Don't include raw exception messages in user responses
Potential enhancements:
- Rate Limiting: Implement per-endpoint rate limiting with
RateLimitError - Error Analytics: Track error patterns for debugging
- Localization: Support multiple languages for error messages
- Error Monitoring: Integration with monitoring services (Sentry, etc.)
- Retry Strategies: More sophisticated retry logic for different error types
- FastAPI Error Handling: https://fastapi.tiangolo.com/tutorial/handling-errors/
- RFC 7807 Problem Details: https://tools.ietf.org/html/rfc7807
- Pydantic Validation: https://docs.pydantic.dev/latest/concepts/validators/