feat: v2 error and debug log structure
This commit is contained in:
parent
6fd97bcbcf
commit
e2714c3e1c
3 changed files with 63 additions and 24 deletions
|
|
@ -488,9 +488,12 @@ def msgprint(
|
||||||
def _raise_exception():
|
def _raise_exception():
|
||||||
if raise_exception:
|
if raise_exception:
|
||||||
if inspect.isclass(raise_exception) and issubclass(raise_exception, Exception):
|
if inspect.isclass(raise_exception) and issubclass(raise_exception, Exception):
|
||||||
raise raise_exception(msg)
|
exc = raise_exception(msg)
|
||||||
else:
|
else:
|
||||||
raise ValidationError(msg)
|
exc = ValidationError(msg)
|
||||||
|
if out.__frappe_exc_id:
|
||||||
|
exc.__frappe_exc_id = out.__frappe_exc_id
|
||||||
|
raise exc
|
||||||
|
|
||||||
if flags.mute_messages:
|
if flags.mute_messages:
|
||||||
_raise_exception()
|
_raise_exception()
|
||||||
|
|
@ -527,6 +530,7 @@ def msgprint(
|
||||||
|
|
||||||
if raise_exception:
|
if raise_exception:
|
||||||
out.raise_exception = 1
|
out.raise_exception = 1
|
||||||
|
out.__frappe_exc_id = generate_hash()
|
||||||
|
|
||||||
if primary_action:
|
if primary_action:
|
||||||
out.primary_action = primary_action
|
out.primary_action = primary_action
|
||||||
|
|
@ -535,10 +539,6 @@ def msgprint(
|
||||||
out.wide = wide
|
out.wide = wide
|
||||||
|
|
||||||
message_log.append(out)
|
message_log.append(out)
|
||||||
|
|
||||||
if raise_exception and hasattr(raise_exception, "__name__"):
|
|
||||||
local.response["exc_type"] = raise_exception.__name__
|
|
||||||
|
|
||||||
_raise_exception()
|
_raise_exception()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -200,7 +200,7 @@ class TestResourceAPI(FrappeAPITestCase):
|
||||||
self.assertIsInstance(json.data, list)
|
self.assertIsInstance(json.data, list)
|
||||||
self.assertIsInstance(json.data[0], list)
|
self.assertIsInstance(json.data[0], list)
|
||||||
|
|
||||||
@parameterize("", "v1", "v2")
|
@parameterize("", "v1")
|
||||||
def test_get_list_debug(self):
|
def test_get_list_debug(self):
|
||||||
# test 5: fetch response with debug
|
# test 5: fetch response with debug
|
||||||
with suppress_stdout():
|
with suppress_stdout():
|
||||||
|
|
@ -253,12 +253,6 @@ class TestResourceAPI(FrappeAPITestCase):
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
self.GENERATED_DOCUMENTS.remove(doc_to_delete)
|
self.GENERATED_DOCUMENTS.remove(doc_to_delete)
|
||||||
|
|
||||||
non_existent_doc = frappe.generate_hash(length=12)
|
|
||||||
with suppress_stdout():
|
|
||||||
response = self.delete(self.resource_path(self.DOCTYPE, non_existent_doc))
|
|
||||||
self.assertEqual(response.status_code, 404)
|
|
||||||
self.assertDictEqual(response.json, {})
|
|
||||||
|
|
||||||
@parameterize("", "v1")
|
@parameterize("", "v1")
|
||||||
def test_run_doc_method(self):
|
def test_run_doc_method(self):
|
||||||
# test 10: Run whitelisted method on doc via /api/resource
|
# test 10: Run whitelisted method on doc via /api/resource
|
||||||
|
|
@ -372,6 +366,15 @@ class TestDocumentAPIV2(TestResourceAPI):
|
||||||
response = self.get(self.resource_path(self.DOCTYPE, random_doc))
|
response = self.get(self.resource_path(self.DOCTYPE, random_doc))
|
||||||
self.assertEqual(response.json["data"]["description"], generated_desc)
|
self.assertEqual(response.json["data"]["description"], generated_desc)
|
||||||
|
|
||||||
|
def test_delete_document_non_existing(self):
|
||||||
|
non_existent_doc = frappe.generate_hash(length=12)
|
||||||
|
with suppress_stdout():
|
||||||
|
response = self.delete(self.resource_path(self.DOCTYPE, non_existent_doc))
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
self.assertEqual(response.json["errors"][0]["type"], "DoesNotExistError")
|
||||||
|
# 404s dont return exceptions
|
||||||
|
self.assertFalse(response.json["errors"][0].get("exception"))
|
||||||
|
|
||||||
|
|
||||||
class TestMethodAPIV2(FrappeAPITestCase):
|
class TestMethodAPIV2(FrappeAPITestCase):
|
||||||
version = "v2"
|
version = "v2"
|
||||||
|
|
@ -479,7 +482,7 @@ class TestMethodAPIV2(FrappeAPITestCase):
|
||||||
response = self.get(
|
response = self.get(
|
||||||
self.method_path(method),
|
self.method_path(method),
|
||||||
{"sid": self.sid, "message": expected_message, "fail": True, "handled": False},
|
{"sid": self.sid, "message": expected_message, "fail": True, "handled": False},
|
||||||
)
|
).json
|
||||||
|
|
||||||
self.assertIsInstance(response["errors"], list)
|
self.assertIsInstance(response["errors"], list)
|
||||||
self.assertEqual(response["errors"][0]["type"], "ZeroDivisionError")
|
self.assertEqual(response["errors"][0]["type"], "ZeroDivisionError")
|
||||||
|
|
@ -522,7 +525,7 @@ class TestReadOnlyMode(FrappeAPITestCase):
|
||||||
self.assertIsInstance(response.json, dict)
|
self.assertIsInstance(response.json, dict)
|
||||||
self.assertIsInstance(response.json["data"], list)
|
self.assertIsInstance(response.json["data"], list)
|
||||||
|
|
||||||
@parameterize("", "v1", "v2")
|
@parameterize("", "v1")
|
||||||
def test_blocked_writes(self):
|
def test_blocked_writes(self):
|
||||||
with suppress_stdout():
|
with suppress_stdout():
|
||||||
response = self.post(
|
response = self.post(
|
||||||
|
|
@ -531,6 +534,15 @@ class TestReadOnlyMode(FrappeAPITestCase):
|
||||||
self.assertEqual(response.status_code, 503)
|
self.assertEqual(response.status_code, 503)
|
||||||
self.assertEqual(response.json["exc_type"], "InReadOnlyMode")
|
self.assertEqual(response.json["exc_type"], "InReadOnlyMode")
|
||||||
|
|
||||||
|
@parameterize("v2")
|
||||||
|
def test_blocked_writes_v2(self):
|
||||||
|
with suppress_stdout():
|
||||||
|
response = self.post(
|
||||||
|
self.resource_path("ToDo"), {"description": frappe.mock("paragraph"), "sid": self.sid}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 503)
|
||||||
|
self.assertEqual(response.json["errors"][0]["type"], "InReadOnlyMode")
|
||||||
|
|
||||||
|
|
||||||
class TestWSGIApp(FrappeAPITestCase):
|
class TestWSGIApp(FrappeAPITestCase):
|
||||||
def test_request_hooks(self):
|
def test_request_hooks(self):
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import decimal
|
||||||
import json
|
import json
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
from typing import TYPE_CHECKING, Literal
|
from typing import TYPE_CHECKING, Literal
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
|
@ -29,22 +30,45 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
def report_error(status_code):
|
def report_error(status_code):
|
||||||
"""Build error. Show traceback in developer mode"""
|
"""Build error. Show traceback in developer mode"""
|
||||||
allow_traceback = frappe.get_system_settings("allow_error_traceback") if frappe.db else False
|
from frappe.api import ApiVersion, get_api_version
|
||||||
if (
|
|
||||||
allow_traceback
|
allow_traceback = (
|
||||||
and (status_code != 404 or frappe.conf.logging)
|
(frappe.get_system_settings("allow_error_traceback") if frappe.db else False)
|
||||||
and not frappe.local.flags.disable_traceback
|
and not frappe.local.flags.disable_traceback
|
||||||
):
|
and (status_code != 404 or frappe.conf.logging)
|
||||||
traceback = frappe.utils.get_traceback()
|
)
|
||||||
if traceback:
|
|
||||||
frappe.errprint(traceback)
|
traceback = frappe.utils.get_traceback()
|
||||||
frappe.local.response.exception = traceback.splitlines()[-1]
|
exc_type, exc_value, _ = sys.exc_info()
|
||||||
|
|
||||||
|
match get_api_version():
|
||||||
|
case ApiVersion.V1:
|
||||||
|
if allow_traceback:
|
||||||
|
frappe.errprint(traceback)
|
||||||
|
frappe.response.exception = traceback.splitlines()[-1]
|
||||||
|
frappe.response["exc_type"] = exc_type.__name__
|
||||||
|
case ApiVersion.V2:
|
||||||
|
error_log = {"type": exc_type.__name__}
|
||||||
|
if allow_traceback:
|
||||||
|
error_log["exception"] = traceback
|
||||||
|
_link_error_with_message_log(error_log, exc_value, frappe.message_log)
|
||||||
|
frappe.local.response.errors = [error_log]
|
||||||
|
|
||||||
response = build_response("json")
|
response = build_response("json")
|
||||||
response.status_code = status_code
|
response.status_code = status_code
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def _link_error_with_message_log(error_log, exception, message_logs):
|
||||||
|
for message in message_logs:
|
||||||
|
if message.get("__frappe_exc_id") == exception.__frappe_exc_id:
|
||||||
|
error_log.update(message)
|
||||||
|
message_logs.remove(message)
|
||||||
|
error_log.pop("raise_exception", None)
|
||||||
|
error_log.pop("__frappe_exc_id", None)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def build_response(response_type=None):
|
def build_response(response_type=None):
|
||||||
if "docs" in frappe.local.response and not frappe.local.response.docs:
|
if "docs" in frappe.local.response and not frappe.local.response.docs:
|
||||||
del frappe.local.response["docs"]
|
del frappe.local.response["docs"]
|
||||||
|
|
@ -164,6 +188,9 @@ def _make_logs_v2():
|
||||||
if frappe.local.message_log:
|
if frappe.local.message_log:
|
||||||
response["messages"] = frappe.local.message_log
|
response["messages"] = frappe.local.message_log
|
||||||
|
|
||||||
|
if frappe.debug_log and frappe.conf.get("logging"):
|
||||||
|
response["debug"] = [{"message": m} for m in frappe.local.debug_log]
|
||||||
|
|
||||||
|
|
||||||
def json_handler(obj):
|
def json_handler(obj):
|
||||||
"""serialize non-serializable data for json"""
|
"""serialize non-serializable data for json"""
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue