diff --git a/mercurial/error.py b/mercurial/error.py --- a/mercurial/error.py +++ b/mercurial/error.py @@ -252,3 +252,6 @@ class UnsupportedBundleSpecification(Exc class CorruptedState(Exception): """error raised when a command is not able to read its state from file""" + +class RichIOError(Abort): + """An IOError that can also have a hint attached.""" diff --git a/mercurial/httppeer.py b/mercurial/httppeer.py --- a/mercurial/httppeer.py +++ b/mercurial/httppeer.py @@ -75,6 +75,41 @@ def encodevalueinheaders(value, header, return result +def _wraphttpresponse(resp): + """Wrap an HTTPResponse with common error handlers. + + This ensures that any I/O from any consumer raises the appropriate + error and messaging. + """ + origread = resp.read + + class readerproxy(resp.__class__): + def read(self, size=None): + try: + return origread(size) + except httplib.IncompleteRead as e: + # e.expected is an integer if length known or None otherwise. + if e.expected: + msg = _('HTTP request error (incomplete response; ' + 'expected %d bytes got %d)') % (e.expected, + len(e.partial)) + else: + msg = _('HTTP request error (incomplete response)') + + raise error.RichIOError( + msg, + hint=_('this may be an intermittent network failure; ' + 'if the error persists, consider contacting the ' + 'network or server operator')) + except httplib.HTTPException as e: + raise error.RichIOError( + _('HTTP request error (%s)') % e, + hint=_('this may be an intermittent failure; ' + 'if the error persists, consider contacting the ' + 'network or server operator')) + + resp.__class__ = readerproxy + class httppeer(wireproto.wirepeer): def __init__(self, ui, path): self.path = path @@ -223,6 +258,10 @@ class httppeer(wireproto.wirepeer): self.ui.debug('http error while sending %s command\n' % cmd) self.ui.traceback() raise IOError(None, inst) + + # Insert error handlers for common I/O failures. + _wraphttpresponse(resp) + # record the url we got redirected to resp_url = resp.geturl() if resp_url.endswith(qs): diff --git a/tests/test-http-bad-server.t b/tests/test-http-bad-server.t --- a/tests/test-http-bad-server.t +++ b/tests/test-http-bad-server.t @@ -267,10 +267,10 @@ Server sends an incomplete capabilities $ hg --config badserver.closeaftersendbytes=180 serve -p $HGPORT -d --pid-file=hg.pid -E error.log $ cat hg.pid > $DAEMON_PIDS -TODO client spews a stack due to uncaught httplib.IncompleteRead - - $ hg clone http://localhost:$HGPORT/ clone 2> /dev/null - [1] + $ hg clone http://localhost:$HGPORT/ clone + abort: HTTP request error (incomplete response; expected 385 bytes got 20) + (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator) + [255] $ killdaemons.py $DAEMON_PIDS @@ -461,11 +461,11 @@ Server sends empty HTTP body for getbund $ hg --config badserver.closeaftersendbytes=933 serve -p $HGPORT -d --pid-file=hg.pid -E error.log $ cat hg.pid > $DAEMON_PIDS -TODO client spews a stack due to uncaught httplib.IncompleteRead - - $ hg clone http://localhost:$HGPORT/ clone 2> /dev/null + $ hg clone http://localhost:$HGPORT/ clone requesting all changes - [1] + abort: HTTP request error (incomplete response) + (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator) + [255] $ killdaemons.py $DAEMON_PIDS @@ -525,11 +525,11 @@ Server sends partial compression string $ hg --config badserver.closeaftersendbytes=945 serve -p $HGPORT -d --pid-file=hg.pid -E error.log $ cat hg.pid > $DAEMON_PIDS -TODO client spews a stack due to uncaught httplib.IncompleteRead - - $ hg clone http://localhost:$HGPORT/ clone 2> /dev/null + $ hg clone http://localhost:$HGPORT/ clone requesting all changes - [1] + abort: HTTP request error (incomplete response; expected 1 bytes got 3) + (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator) + [255] $ killdaemons.py $DAEMON_PIDS @@ -593,7 +593,8 @@ Server sends partial bundle2 header magi $ hg clone http://localhost:$HGPORT/ clone requesting all changes - abort: connection ended unexpectedly + abort: HTTP request error (incomplete response; expected 1 bytes got 3) + (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator) [255] $ killdaemons.py $DAEMON_PIDS @@ -616,7 +617,8 @@ Server sends incomplete bundle2 stream p $ hg clone http://localhost:$HGPORT/ clone requesting all changes - abort: connection ended unexpectedly + abort: HTTP request error (incomplete response; expected 1 bytes got 3) + (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator) [255] $ killdaemons.py $DAEMON_PIDS @@ -640,7 +642,8 @@ Servers stops after bundle2 stream param $ hg clone http://localhost:$HGPORT/ clone requesting all changes - abort: connection ended unexpectedly + abort: HTTP request error (incomplete response) + (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator) [255] $ killdaemons.py $DAEMON_PIDS @@ -664,7 +667,8 @@ Server stops sending after bundle2 part $ hg clone http://localhost:$HGPORT/ clone requesting all changes - abort: connection ended unexpectedly + abort: HTTP request error (incomplete response) + (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator) [255] $ killdaemons.py $DAEMON_PIDS @@ -785,7 +789,8 @@ Server stops sending after 0 length payl added 1 changesets with 1 changes to 1 files transaction abort! rollback completed - abort: connection ended unexpectedly + abort: HTTP request error (incomplete response) + (this may be an intermittent network failure; if the error persists, consider contacting the network or server operator) [255] $ killdaemons.py $DAEMON_PIDS