Repository: mitmproxy/mitmproxy

Issue:
User's script error handler function
As discussed with @cortesi on slack, right now whenever a user's script throws an error due to various reasons, it is being handled at different places differently.
Therefore we can have a consistent error handler function which can be invoked whenever there is an error
This will also handle #2837 #2838 #2839 
### Function

Signature
`script_error(path, message, lineno, exception)`

What function will do
>"Error in script XXX:NNN MMM” where XXX is the path as specified by the user (the .path attribute of Script), NNN is a line number if we have one, and MMM is a short message

The idea here is to display the above mentioned message in the console app and display the traceback related to the error in the event log. 


Discussion:
Hi,
I was working on this and now I think of it, I have many doubts.
I was thinking that instead of keeping the signature as `script_error(path, message, lineno, exception)` let's keep the signature as `script_error_handler(path, exc, tb = False)`
The optional traceback will help us to handle whether to provide the user with the same or not

Log: "Error in Script XXX:NNN MMM", XXX=path, NNN=line, MMM=name of the error thrown"
The function would roughly look like this 
```
def script_error_handler(path, exc, tb=False):
    """
        Handles all the user's script errors with
        an optional traceback
    """
    exception = type(exc).__name__
    lineno = ""
    if hasattr(exc, "lineno"):
        lineno = str(exc.lineno)
    msg = "Error in Script {}:{} {}".format(path, lineno, exception)
    if tb:
        etype, value, tback = sys.exc_info()
        tback = addonmanager.cut_traceback(tback, "invoke_addon")
        msg = msg.join(["\n"]+traceback.format_exception(etype, value, tback))
    ctx.log.error(msg)
```

which can be called like this
```
try:
    something
except Exception as exc:
    script_error_handler(path, exc)
```
@cortesi  what do you think? :confused: 
I think the line number extraction here works - I think a `lineno` attribute is the common API. My rationale for the short message argument is as follows. In the console app, only the first line (or a portion of the first line) of an error is now displayed, and the user has to switch to the event log for everything else. This makes the first line very valuable, and we need to pack as much information as possible in there. It would be nice for this line to say "Syntax error..." and "Exception raised...", and so forth - and in some cases we might be able to give even better information. 

That said, the first step here is to centralise, and then we can tweak the presentation later. Play around until you have a solution that feels right - we can then merge and tweak it. 


Hi, so I had this thought process that since the first line is very critical, we might like to provide the user with the name of the type of error that has been raised and providing any other information in place of this would be a big tradeoff(and fitting more than two words at the end of that line would not be very useful if the user has not maximized the window). 
Maybe in the eventlog we can do like, the important first line with path,line no, name of exception type, second line a useful message and then our optional traceback

Resolving patch:
diff --git a/mitmproxy/addons/script.py b/mitmproxy/addons/script.py
--- a/mitmproxy/addons/script.py
+++ b/mitmproxy/addons/script.py
@@ -5,6 +5,7 @@
 import sys
 import types
 import typing
+import traceback
 
 from mitmproxy import addonmanager
 from mitmproxy import exceptions
@@ -36,6 +37,25 @@ def load_script(path: str) -> types.ModuleType:
         sys.path[:] = oldpath
 
 
+def script_error_handler(path, exc, msg="", tb=False):
+    """
+        Handles all the user's script errors with
+        an optional traceback
+    """
+    exception = type(exc).__name__
+    if msg:
+        exception = msg
+    lineno = ""
+    if hasattr(exc, "lineno"):
+        lineno = str(exc.lineno)
+    log_msg = "in Script {}:{} {}".format(path, lineno, exception)
+    if tb:
+        etype, value, tback = sys.exc_info()
+        tback = addonmanager.cut_traceback(tback, "invoke_addon")
+        log_msg = log_msg.join(["\n"] + traceback.format_exception(etype, value, tback))
+    ctx.log.error(log_msg)
+
+
 class Script:
     """
         An addon that manages a single script.
@@ -53,7 +73,7 @@ def __init__(self, path):
         self.last_load = 0
         self.last_mtime = 0
         if not os.path.isfile(self.fullpath):
-            raise exceptions.OptionsError('No such script: "%s"' % self.fullpath)
+            raise exceptions.OptionsError('No such script')
 
     @property
     def addons(self):
@@ -128,13 +148,13 @@ def script_run(self, flows: typing.Sequence[flow.Flow], path: mtypes.Path) -> No
                 for evt, arg in eventsequence.iterate(f):
                     ctx.master.addons.invoke_addon(s, evt, arg)
         except exceptions.OptionsError as e:
-            raise exceptions.CommandError("Error running script: %s" % e) from e
+            script_error_handler(path, e, msg=str(e))
 
     def configure(self, updated):
         if "scripts" in updated:
             for s in ctx.options.scripts:
                 if ctx.options.scripts.count(s) > 1:
-                    raise exceptions.OptionsError("Duplicate script: %s" % s)
+                    raise exceptions.OptionsError("Duplicate script")
 
             for a in self.addons[:]:
                 if a.path not in ctx.options.scripts:


Test patch:
diff --git a/test/mitmproxy/addons/test_script.py b/test/mitmproxy/addons/test_script.py
--- a/test/mitmproxy/addons/test_script.py
+++ b/test/mitmproxy/addons/test_script.py
@@ -183,9 +183,9 @@ def test_script_run(self):
 
     def test_script_run_nonexistent(self):
         sc = script.ScriptLoader()
-        with taddons.context(sc):
-            with pytest.raises(exceptions.CommandError):
-                sc.script_run([tflow.tflow(resp=True)], "/")
+        with taddons.context(sc) as tctx:
+            sc.script_run([tflow.tflow(resp=True)], "/")
+            tctx.master.has_log("/: No such script")
 
     def test_simple(self):
         sc = script.ScriptLoader()
@@ -243,6 +243,18 @@ def test_load_err(self):
             tctx.invoke(sc, "tick")
             assert len(tctx.master.addons) == 1
 
+    def test_script_error_handler(self):
+        path = "/sample/path/example.py"
+        exc = SyntaxError
+        msg = "Error raised"
+        tb = True
+        with taddons.context() as tctx:
+            script.script_error_handler(path, exc, msg, tb)
+            assert tctx.master.has_log("/sample/path/example.py")
+            assert tctx.master.has_log("Error raised")
+            assert tctx.master.has_log("lineno")
+            assert tctx.master.has_log("NoneType")
+
     def test_order(self):
         rec = tutils.test_data.path("mitmproxy/data/addonscripts/recorder")
         sc = script.ScriptLoader()