Fixed bug  1597381    Map exceptions to status codes
diff --git a/httplib2/__init__.py b/httplib2/__init__.py
index 4cb19f3..6e5973b 100644
--- a/httplib2/__init__.py
+++ b/httplib2/__init__.py
@@ -41,7 +41,7 @@
 import sha
 import hmac
 from gettext import gettext as _
-from socket import gaierror
+import socket
 
 if sys.version_info >= (2,3):
     from iri2uri import iri2uri
@@ -77,11 +77,20 @@
 # All exceptions raised here derive from HttpLib2Error
 class HttpLib2Error(Exception): pass
 
-class RedirectMissingLocation(HttpLib2Error): pass
-class RedirectLimit(HttpLib2Error): pass
-class FailedToDecompressContent(HttpLib2Error): pass
-class UnimplementedDigestAuthOptionError(HttpLib2Error): pass
-class UnimplementedHmacDigestAuthOptionError(HttpLib2Error): pass
+# Some exceptions can be caught and optionally 
+# be turned back into responses. 
+class HttpLib2ErrorWithResponse(HttpLib2Error):
+    def __init__(self, desc, response, content):
+        self.response = response
+        self.content = content
+        HttpLib2Error.__init__(self, desc)
+
+class RedirectMissingLocation(HttpLib2ErrorWithResponse): pass
+class RedirectLimit(HttpLib2ErrorWithResponse): pass
+class FailedToDecompressContent(HttpLib2ErrorWithResponse): pass
+class UnimplementedDigestAuthOptionError(HttpLib2ErrorWithResponse): pass
+class UnimplementedHmacDigestAuthOptionError(HttpLib2ErrorWithResponse): pass
+
 class RelativeURIError(HttpLib2Error): pass
 class ServerNotFoundError(HttpLib2Error): pass
 
@@ -310,7 +319,7 @@
             del response['content-encoding']
     except:
         content = ""
-        raise FailedToDecompressContent(_("Content purported to be compressed with %s but failed to decompress.") % response.get('content-encoding'))
+        raise FailedToDecompressContent(_("Content purported to be compressed with %s but failed to decompress.") % response.get('content-encoding'), response, content)
     return content
 
 def _updateCache(request_headers, response_headers, content, cache, cachekey):
@@ -664,6 +673,8 @@
 
         self.ignore_etag = False
 
+        self.force_exception_to_status_code = True 
+
     def _auth_from_challenge(self, host, request_uri, headers, response, content):
         """A generator that creates Authorization objects
            that can be applied to requests.
@@ -695,9 +706,10 @@
             try:
                 conn.request(method, request_uri, body, headers)
                 response = conn.getresponse()
-            except gaierror:
-                raise ServerNotFoundError("Unable to find the server at %s" % request_uri)
-            except:
+            except socket.gaierror:
+                conn.close()
+                raise ServerNotFoundError("Unable to find the server at %s" % conn.host)
+            except Exception, e:
                 if i == 0:
                     conn.close()
                     conn.connect()
@@ -745,7 +757,7 @@
                 # remembering first to strip the ETag header and decrement our 'depth'
                 if redirections:
                     if not response.has_key('location') and response.status != 300:
-                        raise RedirectMissingLocation( _("Redirected but the response is missing a Location: header."))
+                        raise RedirectMissingLocation( _("Redirected but the response is missing a Location: header."), response, content)
                     # Fix-up relative redirects (which violate an RFC 2616 MUST)
                     if response.has_key('location'):
                         location = response['location']
@@ -770,7 +782,7 @@
                         (response, content) = self.request(location, redirect_method, body=body, headers = headers, redirections = redirections - 1)
                         response.previous = old_response
                 else:
-                    raise RedirectLimit( _("Redirected more times than rediection_limit allows."))
+                    raise RedirectLimit( _("Redirected more times than rediection_limit allows."), response, content)
             elif response.status in [200, 203] and method == "GET":
                 # Don't cache 206's since we aren't going to handle byte range requests
                 if not response.has_key('content-location'):
@@ -800,120 +812,144 @@
 being and instance of the 'Response' class, the second being 
 a string that contains the response entity body.
         """
-        if headers is None:
-            headers = {}
-        else:
-            headers = _normalize_headers(headers)
-
-        if not headers.has_key('user-agent'):
-            headers['user-agent'] = "Python-httplib2/%s" % __version__
-
-        uri = iri2uri(uri)
-
-        (scheme, authority, request_uri, defrag_uri) = urlnorm(uri)
-
-        conn_key = scheme+":"+authority
-        if conn_key in self.connections:
-            conn = self.connections[conn_key]
-        else:
-            connection_type = (scheme == 'https') and httplib.HTTPSConnection or httplib.HTTPConnection
-            certs = list(self.certificates.iter(authority))
-            if scheme == 'https' and certs: 
-                conn = self.connections[conn_key] = connection_type(authority, key_file=certs[0][0], cert_file=certs[0][1])
+        try:
+            if headers is None:
+                headers = {}
             else:
-                conn = self.connections[conn_key] = connection_type(authority)
-            conn.set_debuglevel(debuglevel)
+                headers = _normalize_headers(headers)
 
-        if method in ["GET", "HEAD"] and 'range' not in headers:
-            headers['accept-encoding'] = 'compress, gzip'
+            if not headers.has_key('user-agent'):
+                headers['user-agent'] = "Python-httplib2/%s" % __version__
 
-        info = email.Message.Message()
-        cached_value = None
-        if self.cache:
-            cachekey = defrag_uri
-            cached_value = self.cache.get(cachekey)
-            if cached_value:
-                try:
-                    info = email.message_from_string(cached_value)
-                    content = cached_value.split('\r\n\r\n', 1)[1]
-                except Exception, e:
-                    self.cache.delete(cachekey)
-                    cachekey = None
-                    cached_value = None
-        else:
-            cachekey = None
-                    
-        if method in ["PUT"] and self.cache and info.has_key('etag') and not self.ignore_etag and 'if-match' not in headers:
-            # http://www.w3.org/1999/04/Editing/ 
-            headers['if-match'] = info['etag']
+            uri = iri2uri(uri)
 
-        if method not in ["GET", "HEAD"] and self.cache and cachekey:
-            # RFC 2616 Section 13.10
-            self.cache.delete(cachekey)
+            (scheme, authority, request_uri, defrag_uri) = urlnorm(uri)
 
-        if cached_value and method in ["GET", "HEAD"] and self.cache and 'range' not in headers:
-            if info.has_key('-x-permanent-redirect-url'):
-                # Should cached permanent redirects be counted in our redirection count? For now, yes.
-                (response, new_content) = self.request(info['-x-permanent-redirect-url'], "GET", headers = headers, redirections = redirections - 1)
-                response.previous = Response(info)
-                response.previous.fromcache = True
+            conn_key = scheme+":"+authority
+            if conn_key in self.connections:
+                conn = self.connections[conn_key]
             else:
-                # Determine our course of action:
-                #   Is the cached entry fresh or stale?
-                #   Has the client requested a non-cached response?
-                #   
-                # There seems to be three possible answers: 
-                # 1. [FRESH] Return the cache entry w/o doing a GET
-                # 2. [STALE] Do the GET (but add in cache validators if available)
-                # 3. [TRANSPARENT] Do a GET w/o any cache validators (Cache-Control: no-cache) on the request
-                entry_disposition = _entry_disposition(info, headers) 
-                
-                if entry_disposition == "FRESH":
-                    if not cached_value:
-                        info['status'] = '504'
-                        content = ""
-                    response = Response(info)
-                    if cached_value:
-                        response.fromcache = True
-                    return (response, content)
+                connection_type = (scheme == 'https') and httplib.HTTPSConnection or httplib.HTTPConnection
+                certs = list(self.certificates.iter(authority))
+                if scheme == 'https' and certs: 
+                    conn = self.connections[conn_key] = connection_type(authority, key_file=certs[0][0], cert_file=certs[0][1])
+                else:
+                    conn = self.connections[conn_key] = connection_type(authority)
+                conn.set_debuglevel(debuglevel)
 
-                if entry_disposition == "STALE":
-                    if info.has_key('etag') and not self.ignore_etag and not 'if-none-match' in headers:
-                        headers['if-none-match'] = info['etag']
-                    if info.has_key('last-modified') and not 'last-modified' in headers:
-                        headers['if-modified-since'] = info['last-modified']
-                elif entry_disposition == "TRANSPARENT":
-                    pass
+            if method in ["GET", "HEAD"] and 'range' not in headers:
+                headers['accept-encoding'] = 'compress, gzip'
 
-                (response, new_content) = self._request(conn, authority, uri, request_uri, method, body, headers, redirections, cachekey)
-
-            if response.status == 304 and method == "GET":
-                # Rewrite the cache entry with the new end-to-end headers
-                # Take all headers that are in response 
-                # and overwrite their values in info.
-                # unless they are hop-by-hop, or are listed in the connection header.
-
-                for key in _get_end2end_headers(response):
-                    info[key] = response[key]
-                merged_response = Response(info)
-                if hasattr(response, "_stale_digest"):
-                    merged_response._stale_digest = response._stale_digest
-                try:
-                    _updateCache(headers, merged_response, content, self.cache, cachekey)
-                except:
-                    print locals()
-                    raise 
-                response = merged_response
-                response.status = 200
-                response.fromcache = True 
-
-            elif response.status == 200:
-                content = new_content
+            info = email.Message.Message()
+            cached_value = None
+            if self.cache:
+                cachekey = defrag_uri
+                cached_value = self.cache.get(cachekey)
+                if cached_value:
+                    try:
+                        info = email.message_from_string(cached_value)
+                        content = cached_value.split('\r\n\r\n', 1)[1]
+                    except Exception, e:
+                        self.cache.delete(cachekey)
+                        cachekey = None
+                        cached_value = None
             else:
+                cachekey = None
+                        
+            if method in ["PUT"] and self.cache and info.has_key('etag') and not self.ignore_etag and 'if-match' not in headers:
+                # http://www.w3.org/1999/04/Editing/ 
+                headers['if-match'] = info['etag']
+
+            if method not in ["GET", "HEAD"] and self.cache and cachekey:
+                # RFC 2616 Section 13.10
                 self.cache.delete(cachekey)
-                content = new_content 
-        else: 
-            (response, content) = self._request(conn, authority, uri, request_uri, method, body, headers, redirections, cachekey)
+
+            if cached_value and method in ["GET", "HEAD"] and self.cache and 'range' not in headers:
+                if info.has_key('-x-permanent-redirect-url'):
+                    # Should cached permanent redirects be counted in our redirection count? For now, yes.
+                    (response, new_content) = self.request(info['-x-permanent-redirect-url'], "GET", headers = headers, redirections = redirections - 1)
+                    response.previous = Response(info)
+                    response.previous.fromcache = True
+                else:
+                    # Determine our course of action:
+                    #   Is the cached entry fresh or stale?
+                    #   Has the client requested a non-cached response?
+                    #   
+                    # There seems to be three possible answers: 
+                    # 1. [FRESH] Return the cache entry w/o doing a GET
+                    # 2. [STALE] Do the GET (but add in cache validators if available)
+                    # 3. [TRANSPARENT] Do a GET w/o any cache validators (Cache-Control: no-cache) on the request
+                    entry_disposition = _entry_disposition(info, headers) 
+                    
+                    if entry_disposition == "FRESH":
+                        if not cached_value:
+                            info['status'] = '504'
+                            content = ""
+                        response = Response(info)
+                        if cached_value:
+                            response.fromcache = True
+                        return (response, content)
+
+                    if entry_disposition == "STALE":
+                        if info.has_key('etag') and not self.ignore_etag and not 'if-none-match' in headers:
+                            headers['if-none-match'] = info['etag']
+                        if info.has_key('last-modified') and not 'last-modified' in headers:
+                            headers['if-modified-since'] = info['last-modified']
+                    elif entry_disposition == "TRANSPARENT":
+                        pass
+
+                    (response, new_content) = self._request(conn, authority, uri, request_uri, method, body, headers, redirections, cachekey)
+
+                if response.status == 304 and method == "GET":
+                    # Rewrite the cache entry with the new end-to-end headers
+                    # Take all headers that are in response 
+                    # and overwrite their values in info.
+                    # unless they are hop-by-hop, or are listed in the connection header.
+
+                    for key in _get_end2end_headers(response):
+                        info[key] = response[key]
+                    merged_response = Response(info)
+                    if hasattr(response, "_stale_digest"):
+                        merged_response._stale_digest = response._stale_digest
+                    _updateCache(headers, merged_response, content, self.cache, cachekey)
+                    response = merged_response
+                    response.status = 200
+                    response.fromcache = True 
+
+                elif response.status == 200:
+                    content = new_content
+                else:
+                    self.cache.delete(cachekey)
+                    content = new_content 
+            else: 
+                (response, content) = self._request(conn, authority, uri, request_uri, method, body, headers, redirections, cachekey)
+        except Exception, e:
+            if self.force_exception_to_status_code:
+                if isinstance(e, HttpLib2ErrorWithResponse):
+                    response = e.response
+                    content = e.content
+                    response.status = 500
+                    response.reason = str(e) 
+                elif isinstance(e, socket.timeout):
+                    content = "Request Timeout"
+                    response = Response( {
+                            "content-type": "text/plain",
+                            "status": "408",
+                            "content-length": len(content)
+                            })
+                    response.reason = "Request Timeout"
+                else:
+                    content = str(e) 
+                    response = Response( {
+                            "content-type": "text/plain",
+                            "status": "400",
+                            "content-length": len(content)
+                            })
+                    response.reason = "Bad Request" 
+            else:
+                raise
+
+ 
         return (response, content)
 
  
@@ -949,6 +985,11 @@
             for key, value in info.items(): 
                 self[key] = value 
             self.status = int(self['status'])
+        else:
+            for key, value in info.iteritems(): 
+                self[key] = value 
+            self.status = int(self.get('status', self.status))
+
 
     def __getattr__(self, name):
         if name == 'dict':
diff --git a/httplib2test.py b/httplib2test.py
index d8c1859..6ffc43e 100755
--- a/httplib2test.py
+++ b/httplib2test.py
@@ -111,12 +111,21 @@
         self.http.clear_credentials()
 
     def testGetUnknownServer(self):
+        self.http.force_exception_to_status_code = False 
         try:
             self.http.request("http://fred.bitworking.org/")
             self.fail("An httplib2.ServerNotFoundError Exception must be thrown on an unresolvable server.")
         except httplib2.ServerNotFoundError:
             pass
 
+        # Now test with exceptions turned off
+        self.http.force_exception_to_status_code = True
+
+        (response, content) = self.http.request("http://fred.bitworking.org/")
+        self.assertEqual(response['content-type'], 'text/plain')
+        self.assertTrue(content.startswith("Unable to find"))
+        self.assertEqual(response.status, 400)
+
     def testGetIRI(self):
         if sys.version_info >= (2,3):
             uri = urlparse.urljoin(base, u"reflector/reflector.cgi?d=\N{CYRILLIC CAPITAL LETTER DJE}")
@@ -124,7 +133,6 @@
             d = self.reflector(content)
             self.assertTrue(d.has_key('QUERY_STRING')) 
             self.assertTrue(d['QUERY_STRING'].find('%D0%82') > 0) 
-
     
     def testGetIsDefaultMethod(self):
         # Test that GET is the default method
@@ -260,6 +268,8 @@
         # Test that we can set a lower redirection limit
         # and that we raise an exception when we exceed
         # that limit.
+        self.http.force_exception_to_status_code = False 
+
         uri = urlparse.urljoin(base, "302/twostep.asis")
         try:
             (response, content) = self.http.request(uri, "GET", redirections = 1)
@@ -269,9 +279,20 @@
         except Exception, e:
             self.fail("Threw wrong kind of exception ")
 
+        # Re-run the test with out the exceptions
+        self.http.force_exception_to_status_code = True 
+
+        (response, content) = self.http.request(uri, "GET", redirections = 1)
+        self.assertEqual(response.status, 500)
+        self.assertTrue(response.reason.startswith("Redirected more"))
+        self.assertEqual("302", response['status'])
+        self.assertTrue(content.startswith("<html>"))
+        self.assertTrue(response.previous != None)
+
     def testGet302NoLocation(self):
         # Test that we throw an exception when we get
         # a 302 with no Location: header.
+        self.http.force_exception_to_status_code = False 
         uri = urlparse.urljoin(base, "302/no-location.asis")
         try:
             (response, content) = self.http.request(uri, "GET")
@@ -281,6 +302,15 @@
         except Exception, e:
             self.fail("Threw wrong kind of exception ")
 
+        # Re-run the test with out the exceptions
+        self.http.force_exception_to_status_code = True 
+
+        (response, content) = self.http.request(uri, "GET")
+        self.assertEqual(response.status, 500)
+        self.assertTrue(response.reason.startswith("Redirected but"))
+        self.assertEqual("302", response['status'])
+        self.assertTrue(content.startswith("This is content"))
+ 
     def testGet302ViaHttps(self):
         # Google always redirects to http://google.com
         (response, content) = self.http.request("https://google.com", "GET")
@@ -462,6 +492,7 @@
 
     def testGetGZipFailure(self):
         # Test that we raise a good exception when the gzip fails
+        self.http.force_exception_to_status_code = False 
         uri = urlparse.urljoin(base, "gzip/failed-compression.asis")
         try:
             (response, content) = self.http.request(uri, "GET")
@@ -471,6 +502,27 @@
         except Exception:
             self.fail("Threw wrong kind of exception")
 
+        # Re-run the test with out the exceptions
+        self.http.force_exception_to_status_code = True 
+
+        (response, content) = self.http.request(uri, "GET")
+        self.assertEqual(response.status, 500)
+        self.assertTrue(response.reason.startswith("Content purported"))
+
+    def testTimeout(self):
+        uri = urlparse.urljoin(base, "timeout/timeout.cgi")
+        try:
+            import socket
+            socket.setdefaulttimeout(1) 
+        except:
+            # Don't run the test if we can't set the timeout
+            return 
+        (response, content) = self.http.request(uri)
+        self.assertEqual(response.status, 408)
+        self.assertTrue(response.reason.startswith("Request Timeout"))
+        self.assertTrue(content.startswith("Request Timeout"))
+
+
     def testGetDeflate(self):
         # Test that we support deflate compression
         uri = urlparse.urljoin(base, "deflate/deflated.asis")
@@ -482,7 +534,8 @@
 
     def testGetDeflateFailure(self):
         # Test that we raise a good exception when the deflate fails
-        uri = urlparse.urljoin(base, "deflate/deflated.asis")
+        self.http.force_exception_to_status_code = False 
+
         uri = urlparse.urljoin(base, "deflate/failed-compression.asis")
         try:
             (response, content) = self.http.request(uri, "GET")
@@ -492,6 +545,13 @@
         except Exception:
             self.fail("Threw wrong kind of exception")
 
+        # Re-run the test with out the exceptions
+        self.http.force_exception_to_status_code = True 
+
+        (response, content) = self.http.request(uri, "GET")
+        self.assertEqual(response.status, 500)
+        self.assertTrue(response.reason.startswith("Content purported"))
+
     def testGetDuplicateHeaders(self):
         # Test that duplicate headers get concatenated via ','
         uri = urlparse.urljoin(base, "duplicate-headers/multilink.asis")