"""Structured error envelope + input-size guardrails. Every non-2xx response goes through a single shape: { "error": { "code": "validation_error" | "unauthorized" | ..., "message": "human-readable summary", "request_id": "req_abcd..." # matches X-Request-ID header "details": { ... optional structured detail ... } } } This lets clients branch on ``error.code`` safely. The code is a stable identifier; the message is for humans. ``details`` is where we stuff validation field-paths, retry_after hints, etc. Input limits are enforced centrally so every endpoint gets the same protection against oversized payloads. """ from __future__ import annotations from enum import Enum from typing import Any, Dict, Optional class ErrorCode(str, Enum): VALIDATION_ERROR = "validation_error" UNAUTHORIZED = "unauthorized" ADMIN_REQUIRED = "admin_required" RATE_LIMITED = "rate_limited" NOT_FOUND = "not_found" BAD_STRATEGY = "bad_strategy" PAYLOAD_TOO_LARGE = "payload_too_large" INTERNAL_ERROR = "internal_error" # ------------------------------------------------------------- limits class Limits: """Central knobs — override via env or config before app startup.""" max_query_len: int = 4000 # chars max_docs_per_batch: int = 100 max_doc_text_len: int = 50_000 # chars per document max_k: int = 100 max_session_turns: int = 500 def build_error_body( code: ErrorCode | str, message: str, request_id: Optional[str] = None, details: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: """Return the canonical error envelope shape.""" err: Dict[str, Any] = { "code": code.value if isinstance(code, ErrorCode) else str(code), "message": message, } if request_id: err["request_id"] = request_id if details: err["details"] = details return {"error": err} def validate_query_text(text: str) -> None: if not isinstance(text, str) or not text.strip(): raise ValueError("query text is required") if len(text) > Limits.max_query_len: raise OverflowError( f"query exceeds max length of {Limits.max_query_len} chars" ) def validate_doc_list(docs) -> None: if not isinstance(docs, list): raise ValueError("documents must be a list") if len(docs) > Limits.max_docs_per_batch: raise OverflowError( f"too many documents — max {Limits.max_docs_per_batch} per request" ) for i, d in enumerate(docs): text = getattr(d, "text", None) or (d.get("text") if isinstance(d, dict) else None) if not text or not isinstance(text, str): raise ValueError(f"document[{i}] missing text") if len(text) > Limits.max_doc_text_len: raise OverflowError( f"document[{i}] text exceeds {Limits.max_doc_text_len} chars" ) def validate_k(k: int) -> None: if not isinstance(k, int) or k <= 0: raise ValueError("k must be a positive integer") if k > Limits.max_k: raise OverflowError(f"k exceeds max of {Limits.max_k}")