Coverage for src/local_deep_research/web/exceptions.py: 100%
20 statements
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-19 23:35 +0000
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-19 23:35 +0000
1"""
2Custom exceptions for the web module.
4These exceptions are used to provide structured error handling
5that can be caught by Flask error handlers and converted to
6appropriate JSON responses.
7"""
9from typing import Any, Optional
11from ..security import sanitize_error_details, sanitize_error_for_client
14class WebAPIException(Exception):
15 """Base exception for all web API related errors."""
17 def __init__(
18 self,
19 message: str,
20 status_code: int = 500,
21 error_code: Optional[str] = None,
22 details: Optional[dict[str, Any]] = None,
23 ):
24 """
25 Initialize the web API exception.
27 Args:
28 message: Human-readable error message
29 status_code: HTTP status code for the error
30 error_code: Machine-readable error code for API consumers
31 details: Additional error details/context
32 """
33 super().__init__(message)
34 self.message = message
35 self.status_code = status_code
36 self.error_code = error_code or self.__class__.__name__
37 self.details = details or {}
39 def to_dict(self) -> dict[str, Any]:
40 """Convert exception to dictionary for JSON response.
42 This dict is ``jsonify``-ed straight to the client by the two handlers
43 that catch ``WebAPIException`` (``web/app_factory.py`` and
44 ``research_library/routes/library_routes.py``). As a boundary backstop,
45 the outgoing fields are scrubbed for credential shapes so that a future
46 raise site, subclass, or external caller that lets a credential-bearing
47 string in has it redacted before it ships:
49 * ``message`` — through ``sanitize_error_for_client`` (credential
50 redaction + control-char strip + 200-char cap; it is prose meant for
51 display).
52 * ``details`` string leaves — through ``sanitize_error_details`` (the
53 shared helper, also used by ``NewsAPIException``).
55 Redaction is credential-*shape* based (``Bearer``/``Authorization``,
56 ``?api_key=``-style query params, known token prefixes, URL userinfo),
57 so legitimate text is unchanged. It deliberately does NOT try to catch
58 DSN userinfo beyond URL form, SQL, schema, or filesystem paths. The
59 ``status`` literal and ``error_code`` are left untouched (``status_code``
60 is not part of this body — it is applied by the handler as the HTTP
61 status). ``self.message`` / ``self.details`` are not mutated, so the raw
62 text is retained on the exception object for server-side diagnosis.
63 """
64 result: dict[str, Any] = {
65 "status": "error",
66 "message": sanitize_error_for_client(self.message),
67 "error_code": self.error_code,
68 }
69 if self.details:
70 result["details"] = sanitize_error_details(self.details)
71 return result
74class AuthenticationRequiredError(WebAPIException):
75 """Raised when authentication is required but not available."""
77 def __init__(
78 self,
79 message: str = "Authentication required: Please refresh the page and log in again.",
80 username: Optional[str] = None,
81 ):
82 details = {}
83 if username:
84 details["username"] = username
85 super().__init__(
86 message,
87 status_code=401,
88 error_code="AUTHENTICATION_REQUIRED",
89 details=details,
90 )