wscalls: unify error reporting between wscall objects and actions (#13593) #601

Merged
fpeters merged 1 commits from wip/13593-wscall-errors into main 2023-11-24 16:19:17 +01:00
4 changed files with 89 additions and 40 deletions

View File

@ -235,18 +235,18 @@ def test_webservice_on_error(http_requests, emails, notify_on_errors, record_on_
]:
msg_mapping = {
'400': '400 Bad Request',
'400-json': '400 Bad Request (err_desc :(, err_class foo_bar)',
'400-json': '400 Bad Request (err_desc: :(, err_class: foo_bar)',
'404': '404 Not Found',
'404-json': '404 Not Found (err not-found)',
'404-json': '404 Not Found',
'500': '500 Internal Server Error',
'json-err0': None,
'json-err0int': None,
'json-err1': '(err 1)',
'json-err1int': '(err 1)',
'json-err1-with-desc': '(err_desc :()',
'json-errstr': '(err bug)',
'json-errheader1': '(err 1)',
'json-errheaderstr': '(err bug)',
'json-err1': 'err: 1',
'json-err1int': 'err: 1',
'json-err1-with-desc': 'err: 1, err_desc: :(',
'json-errstr': 'err: bug',
'json-errheader1': 'err: 1',
'json-errheaderstr': 'err: bug',
}
wscall.request = {'url': 'http://remote.example.net/%s' % url_part}
wscall.store()

View File

@ -56,10 +56,10 @@ def test_wscall_record_errors(pub):
wscall.perform(formdata)
assert len([x for x in formdata.evolution[-1].parts if isinstance(x, JournalWsCallErrorPart)]) == 1
assert formdata.evolution[-1].parts[-1].get_json_export_dict() == {
'type': 'wscall-error',
'summary': '404 Not Found',
'label': None,
'data': '{"err": 1}',
'label': None,
'summary': '404 Not Found',
'type': 'wscall-error',
}
# error with bytes that can be stored as string
@ -90,6 +90,47 @@ def test_wscall_record_errors(pub):
'data_b64': '8Q==\n',
}
# application error
pub.loggederror_class.wipe()
with responses.RequestsMock() as rsps:
rsps.get('http://test', status=200, body=b'{"err": 1, "err_desc": "some error"}')
wscall.perform(formdata)
assert formdata.evolution[-1].parts[-1].get_json_export_dict() == {
'data': '{"err": 1, "err_desc": "some error"}',
'label': None,
'summary': 'err: 1, err_desc: some error',
'type': 'wscall-error',
}
assert pub.loggederror_class.count() == 1
assert pub.loggederror_class.select()[0].summary == '[WSCALL] err: 1, err_desc: some error'
pub.loggederror_class.wipe()
with responses.RequestsMock() as rsps:
rsps.get('http://test', status=200, body=b'{"err": 1}')
wscall.perform(formdata)
assert formdata.evolution[-1].parts[-1].get_json_export_dict() == {
'data': '{"err": 1}',
'label': None,
'summary': 'err: 1',
'type': 'wscall-error',
}
assert pub.loggederror_class.count() == 1
assert pub.loggederror_class.select()[0].summary == '[WSCALL] err: 1'
pub.loggederror_class.wipe()
with responses.RequestsMock() as rsps:
rsps.get('http://test', status=200, body=b'xxx', headers={'x-error-code': 'X'})
wscall.perform(formdata)
assert formdata.evolution[-1].parts[-1].get_json_export_dict() == {
'data': 'xxx',
'label': None,
'summary': 'err: X',
'type': 'wscall-error',
}
assert pub.loggederror_class.count() == 1
assert pub.loggederror_class.select()[0].summary == '[WSCALL] err: X'
pub.loggederror_class.wipe()
# error with payload that cannot be converted to JSON
pub.loggederror_class.wipe()
formdata.evolution[-1].parts = []

View File

@ -34,7 +34,7 @@ from wcs.workflows import (
WorkflowStatusItem,
register_item_class,
)
from wcs.wscalls import PayloadError, call_webservice, get_app_error_code
from wcs.wscalls import PayloadError, call_webservice, get_app_error_code, record_wscall_error
from ..qommon import _, force_str, pgettext
from ..qommon.errors import ConnectionError
@ -575,20 +575,25 @@ class WebserviceCallStatusItem(WorkflowStatusItem):
self.notify_on_errors or self.record_on_errors or self.record_errors
):
if exception is None:
summary = '<no response>'
if response is not None:
summary = '%s %s' % (response.status_code, response.reason)
summary = record_wscall_error(
response.status_code,
data,
response,
get_app_error_code(response, data, 'json'),
self.notify_on_errors,
self.record_on_errors,
)
else:
exc_type, exc_value = sys.exc_info()[:2]
summary = traceback.format_exception_only(exc_type, exc_value)[-1]
get_publisher().record_error(
error_summary=summary,
exception=exception,
context='[WSCALL]',
notify=self.notify_on_errors,
record=self.record_on_errors,
)
get_publisher().record_error(
error_summary=summary,
exception=exception,
context='[WSCALL]',
notify=self.notify_on_errors,
record=self.record_on_errors,
)
if self.record_errors and formdata.evolution and not recorded_error:
formdata.evolution[-1].add_part(JournalWsCallErrorPart(summary, self.label, data))
formdata.store()

View File

@ -203,6 +203,13 @@ def call_webservice(
return (None, None, None)
app_error_code = get_app_error_code(response, data, 'json')
if (app_error_code != 0 or status >= 400) and (notify_on_errors or record_on_errors):
record_wscall_error(status, data, response, app_error_code, notify_on_errors, record_on_errors)
return (response, status, data)
def record_wscall_error(status, data, response, app_error_code, notify_on_errors, record_on_errors):
try:
json_data = json_loads(data)
if not isinstance(json_data, dict):
@ -210,24 +217,20 @@ def call_webservice(
except (ValueError, TypeError):
json_data = {}
if (app_error_code != 0 or status >= 400) and (notify_on_errors or record_on_errors):
summary = '<no response>'
if response is not None:
summary = '%s %s' % (status, response.reason) if status != 200 else ''
if app_error_code != 0:
details = []
for key in ['err_desc', 'err_class']:
if json_data.get(key):
details.append('%s %s' % (key, json_data[key]))
if not details or app_error_code != 1:
details.append('err %s' % app_error_code)
details = '(%s)' % ', '.join(details)
summary = '%s %s' % (summary, details) if summary else details
get_publisher().record_error(
summary, context='[WSCALL]', notify=notify_on_errors, record=record_on_errors
)
return (response, status, data)
summary = '<no response>'
if response is not None:
summary = '%s %s' % (status, response.reason) if status != 200 else ''
if app_error_code != 0:
details = [f'err: {app_error_code}'] if status == 200 else []
for key in ['err_desc', 'err_class']:
if json_data.get(key):
details.append('%s: %s' % (key, json_data[key]))
details = ', '.join(details) if details else ''
summary = '%s (%s)' % (summary, details) if (summary and details) else (summary or details)
get_publisher().record_error(
summary, context='[WSCALL]', notify=notify_on_errors, record=record_on_errors
)
return summary
class NamedWsCall(XmlStorableObject):