Repository: algoo/hapic

Issue:
Error catching: erro details must be given by default
When error is catch and transformed to response, error information must be hidden by default. A parameter like "debug" should be able to return it.

Discussion:

Patch:
diff --git a/hapic/context.py b/hapic/context.py
index 97aa0c4..7b5d9b9 100644
--- a/hapic/context.py
+++ b/hapic/context.py
@@ -135,6 +135,15 @@ class ContextInterface(object):
         """
         raise NotImplementedError()
 
+    def is_debug(self) -> bool:
+        """
+        Method called to know if Hapic has been called in debug mode.
+        Debug mode provide some informations like debug trace and error
+        message in body when internal error happen.
+        :return: True if in debug mode
+        """
+        raise NotImplementedError()
+
 
 class HandledException(object):
     """
diff --git a/hapic/decorator.py b/hapic/decorator.py
index 10c6036..8d7f284 100644
--- a/hapic/decorator.py
+++ b/hapic/decorator.py
@@ -420,7 +420,10 @@ class ExceptionHandlerControllerWrapper(ControllerWrapper):
                 func_kwargs,
             )
         except self.handled_exception_class as exc:
-            response_content = self.error_builder.build_from_exception(exc)
+            response_content = self.error_builder.build_from_exception(
+                exc,
+                include_traceback=self.context.is_debug(),
+            )
 
             # Check error format
             dumped = self.error_builder.dump(response_content).data
diff --git a/hapic/error.py b/hapic/error.py
index 9157657..073b849 100644
--- a/hapic/error.py
+++ b/hapic/error.py
@@ -1,4 +1,6 @@
 # -*- coding: utf-8 -*-
+import traceback
+
 import marshmallow
 
 from hapic.processor import ProcessValidationError
@@ -9,7 +11,11 @@ class ErrorBuilderInterface(marshmallow.Schema):
     ErrorBuilder is a class who represent a Schema (marshmallow.Schema) and
     can generate a response content from exception (build_from_exception)
     """
-    def build_from_exception(self, exception: Exception) -> dict:
+    def build_from_exception(
+        self,
+        exception: Exception,
+        include_traceback: bool = False,
+    ) -> dict:
         """
         Build the error response content from given exception
         :param exception: Original exception who invoke this method
@@ -34,14 +40,28 @@ class DefaultErrorBuilder(ErrorBuilderInterface):
     details = marshmallow.fields.Dict(required=False, missing={})
     code = marshmallow.fields.Raw(missing=None)
 
-    def build_from_exception(self, exception: Exception) -> dict:
+    def build_from_exception(
+        self,
+        exception: Exception,
+        include_traceback: bool = False,
+    ) -> dict:
         """
         See hapic.error.ErrorBuilderInterface#build_from_exception docstring
         """
         # TODO: "error_detail" attribute name should be configurable
+        message = str(exception)
+        if not message:
+            message = type(exception).__name__
+
+        details = {
+            'error_detail': getattr(exception, 'error_detail', {}),
+        }
+        if include_traceback:
+            details['traceback'] = traceback.format_exc()
+
         return {
-            'message': str(exception),
-            'details': getattr(exception, 'error_detail', {}),
+            'message': message,
+            'details': details,
             'code': None,
         }
 
diff --git a/hapic/ext/bottle/context.py b/hapic/ext/bottle/context.py
index c5090b8..ba8d75a 100644
--- a/hapic/ext/bottle/context.py
+++ b/hapic/ext/bottle/context.py
@@ -33,12 +33,14 @@ class BottleContext(BaseContext):
         self,
         app: bottle.Bottle,
         default_error_builder: ErrorBuilderInterface=None,
+        debug: bool = False,
     ):
         self._handled_exceptions = []  # type: typing.List[HandledException]  # nopep8
         self._exceptions_handler_installed = False
         self.app = app
         self.default_error_builder = \
             default_error_builder or DefaultErrorBuilder()  # FDV
+        self.debug = debug
 
     def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
         path_parameters = dict(bottle.request.url_args)
@@ -164,3 +166,6 @@ class BottleContext(BaseContext):
         See hapic.context.BaseContext#_get_handled_exception_class_and_http_codes  # nopep8
         """
         return self._handled_exceptions
+
+    def is_debug(self) -> bool:
+        return self.debug
diff --git a/hapic/ext/flask/context.py b/hapic/ext/flask/context.py
index 0908dc2..b548d11 100644
--- a/hapic/ext/flask/context.py
+++ b/hapic/ext/flask/context.py
@@ -32,11 +32,13 @@ class FlaskContext(BaseContext):
         self,
         app: Flask,
         default_error_builder: ErrorBuilderInterface=None,
+        debug: bool = False,
     ):
         self._handled_exceptions = []  # type: typing.List[HandledException]  # nopep8
         self.app = app
         self.default_error_builder = \
             default_error_builder or DefaultErrorBuilder()  # FDV
+        self.debug = debug
 
     def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
         from flask import request
@@ -165,3 +167,6 @@ class FlaskContext(BaseContext):
         http_code: int,
     ) -> None:
         raise NotImplementedError('TODO')
+
+    def is_debug(self) -> bool:
+        return self.debug
diff --git a/hapic/ext/pyramid/context.py b/hapic/ext/pyramid/context.py
index d39b615..6fcde49 100644
--- a/hapic/ext/pyramid/context.py
+++ b/hapic/ext/pyramid/context.py
@@ -31,11 +31,13 @@ class PyramidContext(BaseContext):
         self,
         configurator: 'Configurator',
         default_error_builder: ErrorBuilderInterface = None,
+        debug: bool = False,
     ):
         self._handled_exceptions = []  # type: typing.List[HandledException]  # nopep8
         self.configurator = configurator
         self.default_error_builder = \
             default_error_builder or DefaultErrorBuilder()  # FDV
+        self.debug = debug
 
     def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
         req = args[-1]  # TODO : Check
@@ -189,3 +191,6 @@ class PyramidContext(BaseContext):
         http_code: int,
     ) -> None:
         raise NotImplementedError('TODO')
+
+    def is_debug(self) -> bool:
+        return self.debug


Test patch:
diff --git a/tests/unit/test_decorator.py b/tests/unit/test_decorator.py
index e088a8a..08906a4 100644
--- a/tests/unit/test_decorator.py
+++ b/tests/unit/test_decorator.py
@@ -276,7 +276,7 @@ class TestExceptionHandlerControllerWrapper(Base):
         response = func(42)
         assert HTTPStatus.INTERNAL_SERVER_ERROR == response.status_code
         assert {
-                   'details': {},
+                   'details': {'error_detail': {}},
                    'message': 'We are testing',
                    'code': None,
                } == json.loads(response.body)
@@ -305,7 +305,7 @@ class TestExceptionHandlerControllerWrapper(Base):
         assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR
         assert {
             'message': 'We are testing',
-            'details': {'foo': 'bar'},
+            'details': {'error_detail': {'foo': 'bar'}},
             'code': None,
         } == json.loads(response.body)
 
@@ -314,7 +314,11 @@ class TestExceptionHandlerControllerWrapper(Base):
             pass
 
         class MyErrorBuilder(DefaultErrorBuilder):
-            def build_from_exception(self, exception: Exception) -> dict:
+            def build_from_exception(
+                self,
+                exception: Exception,
+                include_traceback: bool = False,
+            ) -> dict:
                 # this is not matching with DefaultErrorBuilder schema
                 return {}