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

1""" 

2Custom exceptions for the web module. 

3 

4These exceptions are used to provide structured error handling 

5that can be caught by Flask error handlers and converted to 

6appropriate JSON responses. 

7""" 

8 

9from typing import Any, Optional 

10 

11from ..security import sanitize_error_details, sanitize_error_for_client 

12 

13 

14class WebAPIException(Exception): 

15 """Base exception for all web API related errors.""" 

16 

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. 

26 

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 {} 

38 

39 def to_dict(self) -> dict[str, Any]: 

40 """Convert exception to dictionary for JSON response. 

41 

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: 

48 

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``). 

54 

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 

72 

73 

74class AuthenticationRequiredError(WebAPIException): 

75 """Raised when authentication is required but not available.""" 

76 

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 )