hgweb: send proper HTTP response after uncaught exception stable
authorGregory Szorc <gregory.szorc@gmail.com>
Fri, 28 Nov 2014 10:59:02 -0800
branchstable
changeset 23409 dc4d2cd3aa3e
parent 23403 edf29f9c15f0
child 23412 94092019e839
hgweb: send proper HTTP response after uncaught exception This patch fixes a bug where hgweb would send an incomplete HTTP response. If an uncaught exception is raised when hgweb is processing a request, hgweb attempts to send a generic error response and log that exception. The server defaults to chunked transfer coding. If an uncaught exception occurred, it was sending the error response string / chunk properly. However, RFC 7230 Section 4.1 mandates a 0 size last chunk be sent to indicate end of the entity body. hgweb was failing to send this last chunk. As a result, properly written HTTP clients would assume more data was coming and they would likely time out waiting for another chunk to arrive. Mercurial's own test harness was paving over the improper HTTP behavior by not attempting to read the response body if the status code was 500. This incorrect workaround was added in ba6577a19656 and has been removed with this patch.
mercurial/hgweb/server.py
tests/get-with-headers.py
tests/hgweberror.py
tests/test-hgweb.t
--- a/mercurial/hgweb/server.py	Tue Nov 25 19:40:54 2014 -0800
+++ b/mercurial/hgweb/server.py	Fri Nov 28 10:59:02 2014 -0800
@@ -81,6 +81,7 @@
         except Exception:
             self._start_response("500 Internal Server Error", [])
             self._write("Internal Server Error")
+            self._done()
             tb = "".join(traceback.format_exception(*sys.exc_info()))
             self.log_error("Exception happened during processing "
                            "request '%s':\n%s", self.path, tb)
--- a/tests/get-with-headers.py	Tue Nov 25 19:40:54 2014 -0800
+++ b/tests/get-with-headers.py	Fri Nov 28 10:59:02 2014 -0800
@@ -43,9 +43,8 @@
             print "%s: %s" % (h, response.getheader(h))
     if not headeronly:
         print
-        if response.status != 500:
-            data = response.read()
-            sys.stdout.write(data)
+        data = response.read()
+        sys.stdout.write(data)
 
         if twice and response.getheader('ETag', None):
             tag = response.getheader('ETag')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/hgweberror.py	Fri Nov 28 10:59:02 2014 -0800
@@ -0,0 +1,17 @@
+# A dummy extension that installs an hgweb command that throws an Exception.
+
+from mercurial.hgweb import webcommands
+
+def raiseerror(web, req, tmpl):
+    '''Dummy web command that raises an uncaught Exception.'''
+
+    # Simulate an error after partial response.
+    if 'partialresponse' in req.form:
+        req.respond(200, 'text/plain')
+        req.write('partial content\n')
+
+    raise AttributeError('I am an uncaught error!')
+
+def extsetup(ui):
+    setattr(webcommands, 'raiseerror', raiseerror)
+    webcommands.__all__.append('raiseerror')
--- a/tests/test-hgweb.t	Tue Nov 25 19:40:54 2014 -0800
+++ b/tests/test-hgweb.t	Fri Nov 28 10:59:02 2014 -0800
@@ -579,4 +579,30 @@
 
   $ cat errors.log
 
+Uncaught exceptions result in a logged error and canned HTTP response
+
+  $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS
+  $ hg --config extensions.hgweberror=$TESTDIR/hgweberror.py serve -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
+  $ cat hg.pid >> $DAEMON_PIDS
+
+  $ $TESTDIR/get-with-headers.py localhost:$HGPORT 'raiseerror' transfer-encoding content-type
+  500 Internal Server Error
+  transfer-encoding: chunked
+  
+  Internal Server Error (no-eol)
+  [1]
+
+  $ head -1 errors.log
+  .* Exception happened during processing request '/raiseerror': (re)
+
+Uncaught exception after partial content sent
+
+  $ $TESTDIR/get-with-headers.py localhost:$HGPORT 'raiseerror?partialresponse=1' transfer-encoding content-type
+  200 Script output follows
+  transfer-encoding: chunked
+  content-type: text/plain
+  
+  partial content
+  Internal Server Error (no-eol)
+
   $ cd ..