Artur Mukhamadiev e65fbf4b67 backend: FastAPI backend track handoff (contract revision 1)
FastAPI app, upstream Confluence/model adapters, authoritative history,
rootless container lifecycle, artifact storage and downloads, fake peers
under backend/dev, tests under tests/backend. Root pytest.ini deselects
the live marker by default; requirements gain the backend dependencies.
2026-09-14 21:57:54 +03:00

128 lines
4.3 KiB
Python

"""Sanitized application errors and HTTP/protocol mappings."""
from __future__ import annotations
import re
_BEARER_PATTERN = re.compile(r"Bearer\s+[A-Za-z0-9_\-\.]+", re.IGNORECASE)
_TOKEN_PATTERN = re.compile(r"\b[A-Za-z0-9_\-]{32,}\b")
def sanitize_message(msg: str, max_bytes: int = 1024) -> str:
"""Sanitize and bound an error message to max_bytes UTF-8, stripping secret lookalikes."""
if not msg:
return ""
# Strip potential token/secret lookalikes
cleaned = _BEARER_PATTERN.sub("Bearer [REDACTED]", msg)
cleaned = _TOKEN_PATTERN.sub("[REDACTED]", cleaned)
# Strip newlines and carriage returns
cleaned = cleaned.replace("\r", " ").replace("\n", " ")
encoded = cleaned.encode("utf-8")
if len(encoded) <= max_bytes:
return cleaned
truncated = encoded[:max_bytes]
# Ensure valid UTF-8 boundary
return truncated.decode("utf-8", errors="ignore")
class AppError(Exception):
"""Base application error with sanitized error code and message."""
def __init__(self, code: str, message: str, status_code: int = 500):
super().__init__(message)
self.code = code
self.message = sanitize_message(message)
self.status_code = status_code
def to_error_dict(self) -> dict:
return {
"code": self.code,
"message": self.message,
}
def to_envelope(self) -> dict:
return {
"error": self.to_error_dict(),
}
class InvalidInputError(AppError):
def __init__(self, message: str = "Invalid input"):
super().__init__("invalid_input", message, status_code=400)
class ModelContextExceededError(AppError):
def __init__(self, message: str = "Model context window exceeded"):
super().__init__("model_context_exceeded", message, status_code=400)
class OriginDeniedError(AppError):
def __init__(self, message: str = "Origin denied by policy"):
super().__init__("origin_denied", message, status_code=403)
class DestinationDeniedError(AppError):
def __init__(self, message: str = "Destination denied by policy"):
super().__init__("destination_denied", message, status_code=403)
class ConfluenceAuthFailedError(AppError):
def __init__(self, message: str = "Confluence authentication failed"):
super().__init__("confluence_auth_failed", message, status_code=403)
class ArtifactNotFoundError(AppError):
def __init__(self, message: str = "Artifact not found"):
super().__init__("artifact_not_found", message, status_code=404)
class BusyError(AppError):
def __init__(self, message: str = "Active query already in progress"):
super().__init__("busy", message, status_code=409)
class RequestTooLargeError(AppError):
def __init__(self, message: str = "Request body exceeds maximum allowed size"):
super().__init__("request_too_large", message, status_code=413)
class UpstreamFailedError(AppError):
def __init__(self, message: str = "Upstream request failed"):
super().__init__("upstream_failed", message, status_code=502)
class UpstreamResponseTooLargeError(AppError):
def __init__(self, message: str = "Upstream response exceeds maximum allowed size"):
super().__init__("upstream_response_too_large", message, status_code=502)
class TlsFailedError(AppError):
def __init__(self, message: str = "TLS certificate verification failed"):
super().__init__("tls_failed", message, status_code=502)
class ConnectivityFailedError(AppError):
def __init__(self, message: str = "Failed to connect to upstream server"):
super().__init__("connectivity_failed", message, status_code=502)
class ModelOutputLimitError(AppError):
def __init__(self, message: str = "Model output exceeded limit"):
super().__init__("model_output_limit", message, status_code=502)
class QueryTimeoutError(AppError):
def __init__(self, message: str = "Query execution timed out"):
super().__init__("query_timeout", message, status_code=504)
class ExecutionFailedError(AppError):
def __init__(self, message: str = "Query execution failed"):
super().__init__("execution_failed", message, status_code=500)
class CleanupFailedError(AppError):
def __init__(self, message: str = "Container cleanup could not be confirmed"):
super().__init__("cleanup_failed", message, status_code=500)