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
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
1"""Custom exception handling for the DRF API."""
3# -*- coding: utf-8 -*-
5import sys
6import traceback
7from typing import Optional
9from django.conf import settings
10from django.core.exceptions import ValidationError as DjangoValidationError
11from django.utils.translation import gettext_lazy as _
13import structlog
14from rest_framework import serializers
15from rest_framework.exceptions import ValidationError as DRFValidationError
16from rest_framework.response import Response
18logger = structlog.get_logger('inventree')
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.
31 - Uses python exception handling to extract error details
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
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
51 if not path: 51 ↛ 52line 51 didn't jump to line 52 because the condition on line 51 was never true
52 path = ''
54 kind, info, data = sys.exc_info()
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
60 kind = error_name or getattr(kind, '__name__', 'Unknown Error')
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
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'
74 # Log error to stderr
75 logger.error(info)
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}'
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}'
85 # Ensure the error information does not exceed field size limits
86 path = path[:200]
87 kind = kind[:128]
89 try:
90 from error_report.models import Error
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')
98def exception_handler(exc, context):
99 """Custom exception handler for DRF framework.
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.
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
108 import InvenTree.sentry
110 response = None
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
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))
123 # Default to the built-in DRF exception handler
124 response = drfviews.exception_handler(exc, context)
126 if response is None:
127 # DRF handler did not provide a default response for this exception
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')
138 request = context.get('request')
139 path = request.path if request else ''
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 }
149 response = Response(response_data, status=500)
150 log_error(path)
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
156 if data and '__all__' in data:
157 data['non_field_errors'] = data['__all__']
158 del data['__all__']
160 return response