Repository: qutebrowser/qutebrowser

Issue:
AttributeError for downloadProgress
Report on v0.8.4, not sure if it's still current:

```
18:04:39 DEBUG    downloads  downloads:fetch:879 fetch: PyQt5.QtCore.QUrl('https://geekhack.org/attachment.php?attachmentid=49533&d=1335820818') -> attachment.jpg
18:04:39 DEBUG    misc       standarddir:_writable_location:128 writable location for DownloadLocation: /home/dkolomoiets/Downloads
18:04:39 DEBUG    downloads  downloads:set_filename:583 Setting filename to /home/dkolomoiets/Downloads
18:04:39 DEBUG    downloads  downloads:set_fileobj:610 buffer: 0 bytes
18:04:39 DEBUG    downloads  downloads:on_reply_finished:657 Reply finished, fileobj <_io.BufferedWriter name='/home/dkolomoiets/Downloads/attachment.jpg'>
18:04:39 DEBUG    downloads  downloads:finish_download:628 Finishing download...
18:04:39 DEBUG    downloads  downloads:finish_download:639 Download finished
18:04:39 DEBUG    webview    webpage:_handle_errorpage:148 Ignored error on https://geekhack.org/attachment.php?attachmentid=49533&d=1335820818: Frame load interrupted by policy change (error domain: 2, error code: 102)
18:04:39 DEBUG    webview    browsertab:_set_load_status:530 load status for <qutebrowser.browser.webkit.webkittab.WebKitTab tab_id=13 url=''>: LoadStatus.success
18:04:39 DEBUG    signals    signalfilter:_filter_signals:91 ignoring: cur_load_status_changed('success') (tab 1)
18:04:39 DEBUG    signals    signalfilter:_filter_signals:91 ignoring: cur_load_finished(True) (tab 1)
18:04:39 DEBUG    webview    tabbedbrowser:on_title_changed:486 Ignoring title change to ''.
18:04:39 ERROR    misc       crashsignal:exception_hook:211 Uncaught exception
Traceback (most recent call last):
  File "/usr/lib/python3.5/site-packages/qutebrowser/browser/webkit/downloads.py", line 462, in <lambda>
    QTimer.singleShot(0, lambda: self._die(reply.errorString()))
  File "/usr/lib/python3.5/site-packages/qutebrowser/browser/webkit/downloads.py", line 417, in _die
    self.reply.downloadProgress.disconnect()
AttributeError: 'NoneType' object has no attribute 'downloadProgress'
```

cc @Kingdread 


Discussion:
I just reproduced this with a 3/3 success rate on the latest master by just doing `:open https://geekhack.org/attachment.php?attachmentid=49533&d=1335820818`. Using `:download` seems to not crash the browser. Also, it seems like you need `prompt-download-directory` set to false so that the download starts immediately, and the file must not exist (otherwise the download does not start)

```
09:52:40 DEBUG    webview    tabbedbrowser:on_title_changed:523 Changing title for idx 0 to 'https://geekhack.org/attachment.php?attachmentid=49533&d=1335820818'
09:52:40 DEBUG    webview    webpage:acceptNavigationRequest:478 navigation request: url https://geekhack.org/attachment.php?attachmentid=49533&d=1335820818, type NavigationTypeOther, target ClickTarget.normal override None
09:52:40 DEBUG    webview    browsertab:_set_load_status:627 load status for <qutebrowser.browser.webkit.webkittab.WebKitTab tab_id=0 url='https://start.duckduckgo.com/'>: LoadStatus.loading
09:52:40 DEBUG    signals    signalfilter:_filter_signals:86 emitting: cur_load_status_changed('loading') (tab 0)
09:52:40 DEBUG    signals    signalfilter:_filter_signals:86 emitting: cur_load_started() (tab 0)
09:52:40 DEBUG    modes      modeman:leave:294 Ignoring leave request for KeyMode.insert (reason load started) as we're in mode KeyMode.normal
09:52:40 DEBUG    modes      modeman:leave:294 Ignoring leave request for KeyMode.hint (reason load started) as we're in mode KeyMode.normal
09:52:40 DEBUG    modes      modeman:_eventFilter_keypress:192 handled: True, forward-unbound-keys: auto, passthrough: True, is_non_alnum: True --> filter: True (focused: <qutebrowser.browser.webkit.webview.WebView tab_id=0 url='https://start.duckduckgo.com/'>)
09:52:40 DEBUG    modes      modeman:_eventFilter_keyrelease:212 filter: True
09:52:40 DEBUG    rfc6266    http:parse_content_disposition:51 Parsing Content-Disposition: b'inline; filename="/var/www/geekhack.org-html/smf-src-1/attachment.jpg"'
09:52:40 DEBUG    downloads  qtnetworkdownloads:fetch:489 fetch: PyQt5.QtCore.QUrl('https://geekhack.org/attachment.php?attachmentid=49533&d=1335820818') -> attachment.jpg
09:52:40 DEBUG    downloads  downloads:_on_begin_insert_row:821 _on_begin_insert_row with idx 2, webengine False
09:52:40 DEBUG    downloads  downloads:_set_filename:603 Setting filename to I:\Daniel\Downloads\qutebrowser\basedir\download
09:52:40 DEBUG    downloads  qtnetworkdownloads:_set_fileobj:211 buffer: 0 bytes
09:52:40 DEBUG    downloads  qtnetworkdownloads:_on_reply_finished:261 Reply finished, fileobj <_io.BufferedWriter name='I:\\Daniel\\Downloads\\qutebrowser\\basedir\\download\\attachment.jpg'>
09:52:40 DEBUG    downloads  qtnetworkdownloads:_finish_download:232 Finishing download...
09:52:40 DEBUG    downloads  qtnetworkdownloads:_finish_download:243 Download attachment.jpg finished
09:52:40 DEBUG    webview    webpage:_handle_errorpage:147 Ignored error on https://geekhack.org/attachment.php?attachmentid=49533&d=1335820818: Frame load interrupted by policy change (error domain: 2, error code: 102)
09:52:40 DEBUG    sessions   sessions:save:273 Saving session _autosave to I:\Daniel\Downloads\qutebrowser\basedir\data\sessions\_autosave.yml...
09:52:40 DEBUG    webview    browsertab:_set_load_status:627 load status for <qutebrowser.browser.webkit.webkittab.WebKitTab tab_id=0 url='https://start.duckduckgo.com/'>: LoadStatus.success_https
09:52:40 DEBUG    signals    signalfilter:_filter_signals:86 emitting: cur_load_status_changed('success_https') (tab 0)
09:52:40 DEBUG    signals    signalfilter:_filter_signals:86 emitting: cur_load_finished(True) (tab 0)
09:52:40 ERROR    misc       crashsignal:exception_hook:205 Uncaught exception
Traceback (most recent call last):
  File "I:\Daniel\Downloads\qutebrowser\qutebrowser\browser\qtnetworkdownloads.py", line 153, in <lambda>
    QTimer.singleShot(0, lambda: self._die(reply.errorString()))
  File "I:\Daniel\Downloads\qutebrowser\qutebrowser\browser\downloads.py", line 441, in _die
    self._do_die()
  File "I:\Daniel\Downloads\qutebrowser\qutebrowser\browser\qtnetworkdownloads.py", line 113, in _do_die
    self._reply.downloadProgress.disconnect()
AttributeError: 'NoneType' object has no attribute 'downloadProgress'
```

Looks like the download finishes (even though the file has 0 bytes), sets `self._reply` to None and then the error handler still fires.

Resolving patch:
diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py
--- a/qutebrowser/browser/qtnetworkdownloads.py
+++ b/qutebrowser/browser/qtnetworkdownloads.py
@@ -110,6 +110,9 @@ def _create_fileobj(self):
     def _do_die(self):
         """Abort the download and emit an error."""
         self._read_timer.stop()
+        if self._reply is None:
+            log.downloads.debug("Reply gone while dying")
+            return
         self._reply.downloadProgress.disconnect()
         self._reply.finished.disconnect()
         self._reply.error.disconnect()


Test patch:
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
@@ -611,3 +611,9 @@ Feature: Downloading things from a website.
         And I run :follow-hint 0
         And I wait until the download is finished
         Then the downloaded file user-agent should contain Safari/
+
+    @qtwebengine_skip: Handled by QtWebEngine, not by us
+    Scenario: Downloading a "Internal server error" with disposition: inline (#2304)
+        When I set storage -> prompt-download-directory to false
+        And I open custom/500-inline
+        Then the error "Download error: *INTERNAL SERVER ERROR" should be shown
diff --git a/tests/end2end/fixtures/webserver.py b/tests/end2end/fixtures/webserver.py
--- a/tests/end2end/fixtures/webserver.py
+++ b/tests/end2end/fixtures/webserver.py
@@ -74,6 +74,8 @@ def _check_status(self):
             '/redirect-to': [http.client.FOUND],
 
             '/cookies/set': [http.client.FOUND],
+
+            '/custom/500-inline': [http.client.INTERNAL_SERVER_ERROR],
         }
         for i in range(15):
             path_to_statuses['/redirect/{}'.format(i)] = [http.client.FOUND]
diff --git a/tests/end2end/fixtures/webserver_sub.py b/tests/end2end/fixtures/webserver_sub.py
--- a/tests/end2end/fixtures/webserver_sub.py
+++ b/tests/end2end/fixtures/webserver_sub.py
@@ -122,6 +122,17 @@ def redirect_self():
     return app.make_response(flask.redirect(flask.url_for('redirect_self')))
 
 
+@app.route('/custom/500-inline')
+def internal_error_attachment():
+    """A 500 error with Content-Disposition: inline."""
+    response = flask.Response(b"", headers={
+        "Content-Type": "application/octet-stream",
+        "Content-Disposition": 'inline; filename="attachment.jpg"',
+    })
+    response.status_code = 500
+    return response
+
+
 @app.after_request
 def log_request(response):
     """Log a webserver request."""