Repository: mathesar-foundation/mathesar

Issue:
Error handler should not capture exceptions in DEBUG mode
## Description
Based on the [conclusion from the discussion](https://github.com/centerofci/mathesar/discussions/1150).

Improve backend's debugability via two changes:
1. when backend is in debug-mode, add a pretty-printed stacktrace alongside the current error JSON;
2. stop wrapping unhandled exceptions in 4999 errors.

For some context, we're catching backend exceptions and wrapping them in custom errors so as to provide a uniform error interface to API clients. As it is now it has detrimental effects on debugging, because the resulting error messages don't include stacktraces. Above changes aim to address that.


Discussion:
Made changes to top post to include changes decided upon in linked discussion.
May I work on this issue?
Go ahead @vorakunal, thanks!
@vorakunal Are you still working on this?
Unassigned due to inactivity.
Can I work on this issue?

Yes, go ahead @aadityasinha-dotcom. Thanks for your interest!
I'd like to try to solve this issue, if it's still available.
Sure, go ahead @DavidKratochvila 
@kgodey @silentninja 
I'm currently looking into this issue. 
I've tried a couple of things but here's an example of an error before and after my changes.

**Before:**
```json
[
    {
        "code": 4004,
        "message": "CSRF Failed: CSRF token missing or incorrect.",
        "details": {
            "exception": "CSRF Failed: CSRF token missing or incorrect."
        }
    }
]
```



**After:**
```json
[
    {
        "code": 4004,
        "message": "CSRF Failed: CSRF token missing or incorrect.",
        "details": {
            "exception": "CSRF Failed: CSRF token missing or incorrect."
        },
        "stacktrace": [
            "  File \"/usr/local/lib/python3.9/site-packages/rest_framework/views.py\", line 497, in dispatch",
            "    self.initial(request, *args, **kwargs)",
            "  File \"/usr/local/lib/python3.9/site-packages/rest_framework/views.py\", line 414, in initial",
            "    self.perform_authentication(request)",
            "  File \"/usr/local/lib/python3.9/site-packages/rest_framework/views.py\", line 324, in perform_authentication",
            "    request.user",
            "  File \"/usr/local/lib/python3.9/site-packages/rest_framework/request.py\", line 227, in user",
            "    self._authenticate()",
            "  File \"/usr/local/lib/python3.9/site-packages/rest_framework/request.py\", line 380, in _authenticate",
            "    user_auth_tuple = authenticator.authenticate(self)",
            "  File \"/usr/local/lib/python3.9/site-packages/rest_framework/authentication.py\", line 130, in authenticate",
            "    self.enforce_csrf(request)",
            "  File \"/usr/local/lib/python3.9/site-packages/rest_framework/authentication.py\", line 148, in enforce_csrf",
            "    raise exceptions.PermissionDenied('CSRF Failed: %s' % reason)",
            "rest_framework.exceptions.PermissionDenied: CSRF Failed: CSRF token missing or incorrect."
        ]
    }
]
```

This way, when running on Debug mode the developer gets a lot more to work with, which was the intended goal after all. I'm still working on making it better but would appreciate anyone telling me if I'm on the right track and or giving any feedback in general. 

Resolving patch:
diff --git a/mathesar/exception_handlers.py b/mathesar/exception_handlers.py
--- a/mathesar/exception_handlers.py
+++ b/mathesar/exception_handlers.py
@@ -1,4 +1,5 @@
 import warnings
+import traceback
 
 from django.conf import settings
 from django.db import IntegrityError as DjangoIntegrityError
@@ -38,6 +39,9 @@ def standardize_error_response(data):
                 data[index]['code'] = ErrorCodes.UnknownError.value
         if 'detail' not in error:
             data[index]['detail'] = error.pop('details', {})
+            # Adds a Stack-trace of the error for better debugging
+        if settings.MATHESAR_MODE == 'DEVELOPMENT':
+            data[index]['stacktrace'] = reformat_stacktrace(traceback.format_exc())
     return data
 
 
@@ -80,6 +84,9 @@ def mathesar_exception_handler(exc, context):
                 response_data['code'] = error_code
                 response_data['message'] = error_message
                 response_data['details'] = {'exception': force_str(exc)}
+                # Stacktrace should only be returned if MATHESAR_MODE is set to DEVELOPMENT
+                if settings.MATHESAR_MODE == 'DEVELOPMENT':
+                    response_data['stacktrace'] = reformat_stacktrace(traceback.format_exc())
                 response.data = [response_data]
     return response
 
@@ -96,3 +103,8 @@ def is_pretty(data):
             ):
                 return False
         return True
+
+
+def reformat_stacktrace(stacktrace):
+    stacktrace_list = stacktrace.splitlines()[1:]
+    return [f'{i + 1}. {line.strip()}' for i, line in enumerate(stacktrace_list)]


Test patch:
diff --git a/mathesar/tests/api/test_custom_exceptions.py b/mathesar/tests/api/test_custom_exceptions.py
new file mode 100644
--- /dev/null
+++ b/mathesar/tests/api/test_custom_exceptions.py
@@ -0,0 +1,13 @@
+import re
+
+from django.test import override_settings
+
+
+@override_settings(MATHESAR_MODE='DEVELOPMENT')
+def test_exception_stacktrace(client):
+    response = client.post('/api/ui/v0/users/')
+    response_data = response.json()
+    assert response.status_code == 400
+    assert response_data[0]['code'] == 2002
+    assert len(response_data[0]['stacktrace']) >= 1
+    assert bool(re.match('(.*)",(.*)line(.*),(.*)in(.*)', response_data[0]['stacktrace'][0])) is True