Coverage for src/backend/InvenTree/InvenTree/exceptions.py: 73%

71 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 17:47 +0000

1"""Custom exception handling for the DRF API.""" 

2 

3# -*- coding: utf-8 -*- 

4 

5import sys 

6import traceback 

7from typing import Optional 

8 

9from django.conf import settings 

10from django.core.exceptions import ValidationError as DjangoValidationError 

11from django.utils.translation import gettext_lazy as _ 

12 

13import structlog 

14from rest_framework import serializers 

15from rest_framework.exceptions import ValidationError as DRFValidationError 

16from rest_framework.response import Response 

17 

18logger = structlog.get_logger('inventree') 

19 

20 

21def log_error( 

22 path: Optional[str] = None, 

23 error_name: Optional[str] = None, 

24 error_info: Optional[str] = None, 

25 error_data: Optional[str] = None, 

26 scope: Optional[str] = None, 

27 plugin: Optional[str] = None, 

28): 

29 """Log an error to the database. 

30 

31 - Uses python exception handling to extract error details 

32 

33 Arguments: 

34 path: The 'path' (most likely a URL) associated with this error (optional) 

35 error_name: The name of the error (optional, overrides 'kind') 

36 error_info: The error information (optional, overrides 'info') 

37 error_data: The error data (optional, overrides 'data') 

38 scope: The scope of the error (optional) 

39 plugin: The plugin name associated with this error (optional) 

40 """ 

41 import InvenTree.ready 

42 

43 if any([ 43 ↛ 48line 43 didn't jump to line 48 because the condition on line 43 was never true

44 InvenTree.ready.isImportingData(), 

45 InvenTree.ready.isRunningMigrations(), 

46 InvenTree.ready.isRunningBackup(), 

47 ]): 

48 logger.exception('Exception occurred during import, migration, or backup') 

49 return 

50 

51 if not path: 51 ↛ 52line 51 didn't jump to line 52 because the condition on line 51 was never true

52 path = '' 

53 

54 kind, info, data = sys.exc_info() 

55 

56 # Check if the error is on the ignore list 

57 if kind in settings.IGNORED_ERRORS: 57 ↛ 58line 57 didn't jump to line 58 because the condition on line 57 was never true

58 return 

59 

60 kind = error_name or getattr(kind, '__name__', 'Unknown Error') 

61 

62 if error_info: 62 ↛ 63line 62 didn't jump to line 63 because the condition on line 62 was never true

63 info = error_info 

64 

65 if error_data: 65 ↛ 66line 65 didn't jump to line 66 because the condition on line 65 was never true

66 data = error_data 

67 else: 

68 try: 

69 formatted_exception = traceback.format_exception(kind, info, data) 

70 data = '\n'.join(formatted_exception) 

71 except AttributeError: 

72 data = 'No traceback information available' 

73 

74 # Log error to stderr 

75 logger.error(info) 

76 

77 if plugin: 77 ↛ 79line 77 didn't jump to line 79 because the condition on line 77 was never true

78 # If a plugin is specified, prepend it to the path 

79 path = f'plugin.{plugin}.{path}' 

80 

81 if scope: 81 ↛ 83line 81 didn't jump to line 83 because the condition on line 81 was never true

82 # If a scope is specified, prepend it to the path 

83 path = f'{scope}:{path}' 

84 

85 # Ensure the error information does not exceed field size limits 

86 path = path[:200] 

87 kind = kind[:128] 

88 

89 try: 

90 from error_report.models import Error 

91 

92 Error.objects.create(kind=kind, info=info or '', data=data or '', path=path) 

93 except Exception: 

94 # Not much we can do if logging the error throws a db exception 

95 logger.exception('Failed to log exception to database') 

96 

97 

98def exception_handler(exc, context): 

99 """Custom exception handler for DRF framework. 

100 

101 Ref: https://www.django-rest-framework.org/api-guide/exceptions/#custom-exception-handling 

102 Catches any errors not natively handled by DRF, and re-throws as an error DRF can handle. 

103 

104 If sentry error reporting is enabled, we will also provide the original exception to sentry.io 

105 """ 

106 import rest_framework.views as drfviews 

107 

108 import InvenTree.sentry 

109 

110 response = None 

111 

112 # Pass exception to sentry.io handler 

113 try: 

114 InvenTree.sentry.report_exception(exc) 

115 except Exception: 

116 # If sentry.io fails, we don't want to crash the server! 

117 pass 

118 

119 # Catch any django validation error, and re-throw a DRF validation error 

120 if isinstance(exc, DjangoValidationError): 

121 exc = DRFValidationError(detail=serializers.as_serializer_error(exc)) 

122 

123 # Default to the built-in DRF exception handler 

124 response = drfviews.exception_handler(exc, context) 

125 

126 if response is None: 

127 # DRF handler did not provide a default response for this exception 

128 

129 if settings.TESTING: 129 ↛ 131line 129 didn't jump to line 131 because the condition on line 129 was never true

130 # If in TESTING mode, re-throw the exception for traceback 

131 raise exc 

132 elif settings.DEBUG: 132 ↛ 134line 132 didn't jump to line 134 because the condition on line 132 was never true

133 # If in DEBUG mode, provide error information in the response 

134 error_detail = str(exc) 

135 else: 

136 error_detail = _('Error details can be found in the admin panel') 

137 

138 request = context.get('request') 

139 path = request.path if request else '' 

140 

141 response_data = { 

142 'error': type(exc).__name__, 

143 'error_class': str(type(exc)), 

144 'detail': error_detail, 

145 'path': path, 

146 'status_code': 500, 

147 } 

148 

149 response = Response(response_data, status=500) 

150 log_error(path) 

151 

152 if response is not None: 152 ↛ 160line 152 didn't jump to line 160 because the condition on line 152 was always true

153 # Convert errors returned under the label '__all__' to 'non_field_errors' 

154 data = response.data 

155 

156 if data and '__all__' in data: 

157 data['non_field_errors'] = data['__all__'] 

158 del data['__all__'] 

159 

160 return response