Repository: qutebrowser/qutebrowser

Issue:
KeyError in mhtml.finished
Report on 0f8b298 without debug log and "`:download-cancel`" as report message:

```
Traceback (most recent call last):
  File "/usr/lib64/python3.4/site-packages/qutebrowser/browser/mhtml.py", line 357, in _finished
    self.pending_downloads.remove((url, item))
KeyError: (PyQt5.QtCore.QUrl('https://stribika.github.io/assets/nsa-happy-dance.png'), <qutebrowser.browser.downloads.DownloadItem basename='nsa-happy-dance.png'>)
```

Without cssutils installed FWIW.

@Kingdread want to take a look?



Discussion:
I can reproduce this by doing `:download --mhtml ;; :later 2000 download-cancel` on this page.

`download.cancel()` emits both, `finished` and `cancelled`, which leads to both handlers being called, `_finished` and `_error`, and since `_error` is called before `_finished`, the download is removed from the list and `_finish` fails... which leads me to the question: If a download is cancelled, should it just use an empty file (like it does on error) or should it cancel the whole mhtml download?

Hmm, good question... From an user's perspective I think both would be useful. Assuming an empty file if the user sees some big download but knows they don't need that one part, or cancelling the whole download if they accidentally download the wrong page or whatever.

Any other opinions? If not I'd say let's just assume an empty file because it's probably easier to implement :wink:

Cancelling the whole thing shouldn't be that difficult either, but the "disregard that single file" is probably useful, especially because there's no other mechanism for excluding elements.

On the other hand, we currently only download `img`/`link`/`script` elements (no `video` or something like that), so the amount of big files is probably pretty small.

That's certainly what I'd expect, but you newer know with today's crazy web


Resolving patch:
diff --git a/qutebrowser/browser/mhtml.py b/qutebrowser/browser/mhtml.py
--- a/qutebrowser/browser/mhtml.py
+++ b/qutebrowser/browser/mhtml.py
@@ -345,7 +345,7 @@ def _fetch_url(self, url):
         self.pending_downloads.add((url, item))
         item.finished.connect(functools.partial(self._finished, url, item))
         item.error.connect(functools.partial(self._error, url, item))
-        item.cancelled.connect(functools.partial(self._error, url, item))
+        item.cancelled.connect(functools.partial(self._cancelled, url, item))
 
     def _finished(self, url, item):
         """Callback when a single asset is downloaded.
@@ -418,6 +418,20 @@ def _error(self, url, item, *_args):
             return
         self._finish_file()
 
+    def _cancelled(self, url, item):
+        """Callback when a download is cancelled by the user.
+
+        Args:
+            url: The original url of the asset as QUrl.
+            item: The DownloadItem given by the DownloadManager.
+        """
+        # This callback is called before _finished, so there's no need to
+        # remove the item or close the fileobject.
+        log.downloads.debug("MHTML download cancelled by user: {}".format(url))
+        # Write an empty file instead
+        item.fileobj.seek(0)
+        item.fileobj.truncate()
+
     def _finish_file(self):
         """Save the file to the filename given in __init__."""
         if self._finished_file:


Test patch:
diff --git a/tests/end2end/data/downloads/issue1535.html b/tests/end2end/data/downloads/issue1535.html
new file mode 100644
--- /dev/null
+++ b/tests/end2end/data/downloads/issue1535.html
@@ -0,0 +1,14 @@
+<html>
+    <head>
+        <title>test case for issue 1535</title>
+    </head>
+    <body>
+        <p>Cancelling a download that belongs to a mhtml download.</p>
+        <p>See also <a href="https://github.com/The-Compiler/qutebrowser/issues/1535">GitHub</a></p>
+        <!--
+            Use drip so we have a chance to cancel the download before
+            it finishes
+        -->
+        <img src="/drip?numbytes=128&duration=2">
+    </body>
+</html>
diff --git a/tests/end2end/features/downloads.feature b/tests/end2end/features/downloads.feature
--- a/tests/end2end/features/downloads.feature
+++ b/tests/end2end/features/downloads.feature
@@ -126,6 +126,13 @@ Feature: Downloading things from a website.
         Then "cancelled" should be logged
         And "cancelled" should be logged
 
+    # https://github.com/The-Compiler/qutebrowser/issues/1535
+    Scenario: Cancelling a MHTML download (issue 1535)
+        When I open data/downloads/issue1535.html
+        And I run :download --mhtml
+        And I run :download-cancel
+        Then no crash should happen
+
     ## :download-delete
 
     Scenario: Deleting a download