Repository: mirumee/ariadne

Issue:
Exceptions are never delivered to extensions
If you add an extension that implements the `def request_finished(self, context: ContextValue, error: Optional[Exception] = None)` method, and raise any kind of Exception in the resolver, the `request_finished` method of the extension always gets called without any errors.

A stack trace shows that the request context manager in `extensions.py` never catches the exception, so the `else` statement is always executed no matter what kind of exception I raise.


Discussion:
This signature looks wrong, all exceptions are handled by the executor so `errors` would have to be a list since technically all resolver can raise without preventing the execution of the rest of the query.
Yup, that one is not going to work, but we have `Extension.has_errors()` that will be called with a list of errors by our error/response handlers.

Resolving patch:
diff --git a/ariadne/contrib/tracing/opentracing.py b/ariadne/contrib/tracing/opentracing.py
--- a/ariadne/contrib/tracing/opentracing.py
+++ b/ariadne/contrib/tracing/opentracing.py
@@ -27,9 +27,7 @@ def request_started(self, context: ContextValue):
         self._root_scope = self._tracer.start_active_span("GraphQL Query")
         self._root_scope.span.set_tag(tags.COMPONENT, "graphql")
 
-    def request_finished(
-        self, context: ContextValue, error: Optional[Exception] = None
-    ):
+    def request_finished(self, context: ContextValue):
         self._root_scope.close()
 
     async def resolve(
diff --git a/ariadne/extensions.py b/ariadne/extensions.py
--- a/ariadne/extensions.py
+++ b/ariadne/extensions.py
@@ -30,11 +30,7 @@ def request(self, context: ContextValue):
             ext.request_started(context)
         try:
             yield
-        except Exception as e:
-            for ext in self.extensions_reversed:
-                ext.request_finished(context, e)
-            raise
-        else:
+        finally:
             for ext in self.extensions_reversed:
                 ext.request_finished(context)
 
diff --git a/ariadne/types.py b/ariadne/types.py
--- a/ariadne/types.py
+++ b/ariadne/types.py
@@ -29,9 +29,7 @@ class Extension(Protocol):
     def request_started(self, context: ContextValue):
         pass  # pragma: no cover
 
-    def request_finished(
-        self, context: ContextValue, error: Optional[Exception] = None
-    ):
+    def request_finished(self, context: ContextValue):
         pass  # pragma: no cover
 
     async def resolve(


Test patch:
diff --git a/tests/test_extensions.py b/tests/test_extensions.py
--- a/tests/test_extensions.py
+++ b/tests/test_extensions.py
@@ -1,12 +1,13 @@
 from unittest.mock import Mock
 
-from ariadne import ExtensionManager
+import pytest
+
+from ariadne import ExtensionManager, graphql
 from ariadne.types import Extension
 
 
 context = {}
 exception = ValueError()
-query = "{ test }"
 
 
 def test_request_started_event_is_called_by_extension_manager():
@@ -27,18 +28,6 @@ def test_request_finished_event_is_called_by_extension_manager():
     extension.request_finished.assert_called_once_with(context)
 
 
-def test_request_finished_event_is_called_with_error():
-    extension = Mock(spec=Extension)
-    manager = ExtensionManager([Mock(return_value=extension)])
-    try:
-        with manager.request(context):
-            raise exception
-    except:  # pylint: disable=bare-except
-        pass
-
-    extension.request_finished.assert_called_once_with(context, exception)
-
-
 def test_has_errors_event_is_called_with_errors_list():
     extension = Mock(spec=Extension)
     manager = ExtensionManager([Mock(return_value=extension)])
@@ -53,3 +42,15 @@ def test_extensions_are_formatted():
     ]
     manager = ExtensionManager([Mock(return_value=ext) for ext in extensions])
     assert manager.format() == {"a": 1, "b": 2}
+
+
+class BaseExtension(Extension):
+    pass
+
+
+@pytest.mark.asyncio
+async def test_default_extension_hooks_dont_interrupt_query_execution(schema):
+    _, response = await graphql(
+        schema, {"query": "{ status }"}, extensions=[BaseExtension]
+    )
+    assert response["data"] == {"status": True}
diff --git a/tests/test_extensions_sync.py b/tests/test_extensions_sync.py
--- a/tests/test_extensions_sync.py
+++ b/tests/test_extensions_sync.py
@@ -1,16 +1,15 @@
 from unittest.mock import Mock
 
-from ariadne import ExtensionManager
-from ariadne.types import ExtensionSync as Extension
+from ariadne import ExtensionManager, graphql_sync
+from ariadne.types import ExtensionSync
 
 
 context: dict = {}
 exception = ValueError()
-query = "{ test }"
 
 
 def test_request_started_event_is_called_by_extension_manager():
-    extension = Mock(spec=Extension)
+    extension = Mock(spec=ExtensionSync)
     manager = ExtensionManager([Mock(return_value=extension)])
     with manager.request(context):
         pass
@@ -19,7 +18,7 @@ def test_request_started_event_is_called_by_extension_manager():
 
 
 def test_request_finished_event_is_called_by_extension_manager():
-    extension = Mock(spec=Extension)
+    extension = Mock(spec=ExtensionSync)
     manager = ExtensionManager([Mock(return_value=extension)])
     with manager.request(context):
         pass
@@ -27,20 +26,8 @@ def test_request_finished_event_is_called_by_extension_manager():
     extension.request_finished.assert_called_once_with(context)
 
 
-def test_request_finished_event_is_called_with_error():
-    extension = Mock(spec=Extension)
-    manager = ExtensionManager([Mock(return_value=extension)])
-    try:
-        with manager.request(context):
-            raise exception
-    except:  # pylint: disable=bare-except
-        pass
-
-    extension.request_finished.assert_called_once_with(context, exception)
-
-
 def test_has_errors_event_is_called_with_errors_list():
-    extension = Mock(spec=Extension)
+    extension = Mock(spec=ExtensionSync)
     manager = ExtensionManager([Mock(return_value=extension)])
     manager.has_errors([exception])
     extension.has_errors.assert_called_once_with([exception])
@@ -48,8 +35,15 @@ def test_has_errors_event_is_called_with_errors_list():
 
 def test_extensions_are_formatted():
     extensions = [
-        Mock(spec=Extension, format=Mock(return_value={"a": 1})),
-        Mock(spec=Extension, format=Mock(return_value={"b": 2})),
+        Mock(spec=ExtensionSync, format=Mock(return_value={"a": 1})),
+        Mock(spec=ExtensionSync, format=Mock(return_value={"b": 2})),
     ]
     manager = ExtensionManager([Mock(return_value=ext) for ext in extensions])
     assert manager.format() == {"a": 1, "b": 2}
+
+
+def test_default_extension_hooks_dont_interrupt_query_execution(schema):
+    _, response = graphql_sync(
+        schema, {"query": "{ status }"}, extensions=[ExtensionSync]
+    )
+    assert response["data"] == {"status": True}
diff --git a/tests/tracing/snapshots/snap_test_apollotracing_sync.py b/tests/tracing/snapshots/snap_test_apollotracing_sync.py
new file mode 100644
--- /dev/null
+++ b/tests/tracing/snapshots/snap_test_apollotracing_sync.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+# snapshottest: v1 - https://goo.gl/zC4yUc
+from __future__ import unicode_literals
+
+from snapshottest import Snapshot
+
+
+snapshots = Snapshot()
+
+snapshots['test_apollotracing_extension_adds_tracing_data_to_result_extensions 1'] = {
+    'data': {
+        'status': True
+    },
+    'extensions': {
+        'tracing': {
+            'duration': 0,
+            'endTime': '2012-01-14T03:21:34.000000Z',
+            'execution': {
+                'resolvers': [
+                    {
+                        'duration': 0,
+                        'fieldName': 'status',
+                        'parentType': 'Query',
+                        'path': [
+                            'status'
+                        ],
+                        'returnType': 'Boolean',
+                        'startOffset': 0
+                    }
+                ]
+            },
+            'startTime': '2012-01-14T03:21:34.000000Z',
+            'version': 1
+        }
+    }
+}
+
+snapshots['test_apollotracing_extension_handles_exceptions_in_resolvers 1'] = {
+    'testError': None
+}