diff --git a/docs/source/application_development/web_development.rst b/docs/source/application_development/web_development.rst index 1f051731b..62278f055 100644 --- a/docs/source/application_development/web_development.rst +++ b/docs/source/application_development/web_development.rst @@ -514,6 +514,7 @@ Some common returned status codes you may encounter are: - ``400 BAD REQUEST`` -- The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing). - ``401 UNAUTHORIZED`` -- Authentication is required and the request has failed to provide valid authentication credentials. +- ``404 NOT FOUND`` -- Request could not be completed because requested resources could not be found. - ``500 INTERNAL SERVER ERROR`` -- The server encountered an unexpected condition that prevented it from fulfilling the request. diff --git a/nucypher/control/controllers.py b/nucypher/control/controllers.py index 564941166..0a85800d9 100644 --- a/nucypher/control/controllers.py +++ b/nucypher/control/controllers.py @@ -29,6 +29,7 @@ from hendrix.deploy.base import HendrixDeploy from hendrix.deploy.tls import HendrixDeployTLS from twisted.internet import reactor, stdio +from nucypher.cli.processes import JSONRPCLineReceiver from nucypher.config.constants import MAX_UPLOAD_CONTENT_LENGTH from nucypher.control.emitters import StdoutEmitter, JSONRPCStdoutEmitter, WebEmitter from nucypher.control.interfaces import ControlInterface @@ -250,6 +251,7 @@ class WebController(InterfaceControlServer): _captured_status_codes = {200: 'OK', 400: 'BAD REQUEST', + 404: 'NOT FOUND', 500: 'INTERNAL SERVER ERROR'} def test_client(self): @@ -302,8 +304,27 @@ class WebController(InterfaceControlServer): def __call__(self, *args, **kwargs): return self.handle_request(*args, **kwargs) - def handle_request(self, method_name, control_request, *args, **kwargs) -> Response: + @staticmethod + def json_response_from_worker_pool_exception(exception): + json_response = {} + if isinstance(exception, WorkerPool.TimedOut): + message_prefix = f"Execution timed out after {exception.timeout}s" + else: + message_prefix = f"Execution failed - no more values to try" + json_response['failure_message'] = f"{message_prefix}; " \ + f"{len(exception.failures)} concurrent failures recorded" + if exception.failures: + failures = [] + for value, exc_info in exception.failures.items(): + failure = {'value': value} + _, exception, tb = exc_info + failure['error'] = str(exception) + failures.append(failure) + json_response['failures'] = failures + return json_response + + def handle_request(self, method_name, control_request, *args, **kwargs) -> Response: _400_exceptions = (SpecificationError, TypeError, JSONDecodeError, @@ -336,47 +357,26 @@ class WebController(InterfaceControlServer): error_message=WebController._captured_status_codes[__exception_code]) # - # Server Errors + # Execution Errors # - except SpecificationError as e: - __exception_code = 500 + except WorkerPoolException as e: + # special case since WorkerPoolException contains multiple stack traces + # - not ideal for returning from REST endpoints + __exception_code = 404 if self.crash_on_error: raise - return self.emitter.exception( - e=e, - log_level='critical', - response_code=__exception_code, - error_message=WebController._captured_status_codes[__exception_code]) + + json_response_from_exception = self.json_response_from_worker_pool_exception(e) + return self.emitter.exception_with_response( + json_error_response=json_response_from_exception, + e=RuntimeError(json_response_from_exception['failure_message']), + error_message=WebController._captured_status_codes[__exception_code], + log_level='warn', + response_code=__exception_code) # # Unhandled Server Errors # - except WorkerPoolException as e: - # special case since WorkerPoolException contain stack traces - not ideal for returning from REST endpoints - __exception_code = 500 - if self.crash_on_error: - raise - - if isinstance(e, WorkerPool.TimedOut): - message_prefix = f"Execution timed out after {e.timeout}s" - else: - message_prefix = f"Execution failed - no more values to try" - - # get random failure for context - if e.failures: - value = list(e.failures)[0] - _, exception, _ = e.failures[value] - msg = f"{message_prefix} ({len(e.failures)} concurrent failures recorded); " \ - f"for example, for {value}: {exception}" - else: - msg = message_prefix - - return self.emitter.exception( - e=RuntimeError(msg), - log_level='warn', - response_code=__exception_code, - error_message=WebController._captured_status_codes[__exception_code]) - except Exception as e: __exception_code = 500 if self.crash_on_error: @@ -392,4 +392,4 @@ class WebController(InterfaceControlServer): # else: self.log.debug(f"{method_name} [200 - OK]") - return self.emitter.respond(response=response) + return self.emitter.respond(json_response=response) diff --git a/nucypher/control/emitters.py b/nucypher/control/emitters.py index 9ca11d439..79090b65f 100644 --- a/nucypher/control/emitters.py +++ b/nucypher/control/emitters.py @@ -243,6 +243,14 @@ class WebEmitter: self.log = Logger('web-emitter') + def _log_exception(self, e, error_message, log_level, response_code): + exception = f"{type(e).__name__}: {str(e)}" if str(e) else type(e).__name__ + message = f"{self} [{str(response_code)} - {error_message}] | ERROR: {exception}" + logger = getattr(self.log, log_level) + # See #724 / 2156 + message_cleaned_for_logger = message.replace("{", "<^<").replace("}", ">^>") + logger(message_cleaned_for_logger) + @staticmethod def assemble_response(response: dict) -> dict: response_data = {'result': response, @@ -255,25 +263,35 @@ class WebEmitter: log_level: str = 'info', response_code: int = 500): - exception = f"{type(e).__name__}: {str(e)}" if str(e) else type(e).__name__ - message = f"{self} [{str(response_code)} - {error_message}] | ERROR: {exception}" - logger = getattr(self.log, log_level) - # See #724 / 2156 - message_cleaned_for_logger = message.replace("{", "<^<").replace("}", ">^>") - logger(message_cleaned_for_logger) + self._log_exception(e, error_message, log_level, response_code) if self.crash_on_error: raise e response_message = str(e) or type(e).__name__ return self.sink(response_message, status=response_code) - def respond(self, response) -> Response: - assembled_response = self.assemble_response(response=response) + def exception_with_response(self, + json_error_response, + e, + error_message: str, + log_level: str = 'info', + response_code: int = 500): + self._log_exception(e, error_message, log_level, response_code) + if self.crash_on_error: + raise e + + assembled_response = self.assemble_response(response=json_error_response) serialized_response = WebEmitter.transport_serializer(assembled_response) - # ---------- HTTP OUTPUT - response = self.sink(response=serialized_response, status=HTTPStatus.OK, content_type="application/json") - return response + json_response = self.sink(response=serialized_response, status=response_code, content_type="application/json") + return json_response + + def respond(self, json_response) -> Response: + assembled_response = self.assemble_response(response=json_response) + serialized_response = WebEmitter.transport_serializer(assembled_response) + + json_response = self.sink(response=serialized_response, status=HTTPStatus.OK, content_type="application/json") + return json_response def get_stream(self, *args, **kwargs): return null_stream()