Repository: googleapis/google-api-python-client

Issue:
Add exception handling to docs
Hi :) 


I was reading the [docs](https://github.com/googleapis/google-api-python-client/blob/master/docs/start.md) looking for an example to handle exceptions from when request.execute() goes wrong e.g. a 403 due to 
Exceeding qouta limits.


I would like for the docs to be updated with a try: and except: like this
``` python
    try:
        response = request.execute()
    except HttpError as e:
        logger.error('Error response status code %d, reason %s:', e.resp.status, e.content)
        return {'error': 403, 'body' : 'YouTube API Data v3 qouta limit exceeded'}
```
or something else in the `except` block
 
If you're happy with this I'd like to contribute this as a first timer to open source?



Discussion:
Hey @mm326 ,

Thanks for suggesting an update to the docs! A pull request for the docs would be very welcome! My initial thought is to modify the except block to be more generic and just let the specific quota error message bubble up. I'd suggest to use `e.error_details` instead of `e.content` as the output of `e.content` seems overwhelming.

For example,
```
try:
    response = request.execute()
except HttpError as e:
    print('Error response status code : {0}, reason : {1}'.format(e.resp.status, e.error_details))
```

I tried sample code from [YouTube API Data v3](https://developers.google.com/youtube/v3/quickstart/python) and sent a bunch of requests to exceed my YouTube quota limit to test the output of the proposed docs update.  After the quota was exceeded, I ran the above code and found that `e.error_details` is empty. We would need to make an improvement to the library to improve how `error_details` attribute is populated. Are you by any chance interested in making changes to the error parsing in the library? The code that does the error parsing is in function `_get_reason()` function of `HttpError` located [here](https://github.com/googleapis/google-api-python-client/blob/master/googleapiclient/errors.py#L47) .


Looking at the the [error parsing code](https://github.com/googleapis/google-api-python-client/blob/master/googleapiclient/errors.py#L54), the code is only looking for either `details` or `detail` as the keyword for looking for detailed error information which isn't part of the response that we get so the `error_details` attribute in `HttpError` isn't populated. My initial thought is that it would be better to at least populate the error_details with whatever is in `message` rather than have it blank.

```
>>> data = json.loads(self.content.decode("utf-8"))
>>> data['error']
{'code': 403, 'message': 'The request cannot be completed because you have exceeded your <a href="/youtube/v3/getting-started#quota">quota</a>.', 'errors': [{'message': 'The request cannot be completed because you have exceeded your <a href="/youtube/v3/getting-started#quota">quota</a>.', 'domain': 'youtube.quota', 'reason': 'quotaExceeded'}]}
```

I managed to hack together some code to determine the best error detail keyword to use and then populate the `error_details` attribute.

Existing code
```
    def _get_reason(self):
        """Calculate the reason for the error from the response content."""
        reason = self.resp.reason
        try:
            data = json.loads(self.content.decode("utf-8"))
            if isinstance(data, dict):
                reason = data["error"]["message"]
                if "details" in data["error"]:
                    self.error_details = data["error"]["details"]
                elif "detail" in data["error"]:
                    self.error_details = data["error"]["detail"]
```

Improved code 
```
    def _get_reason(self):
        """Calculate the reason for the error from the response content."""
        reason = self.resp.reason
        try:
            data = json.loads(self.content.decode("utf-8"))
            if isinstance(data, dict):
                reason = data["error"]["message"]
                # APIs are not consistent in the error detail keyword
                # so determine the appropriate error detail keyword
                error_detail_keyword = next((kw for kw in ['detail', 'details', 'message'] if kw in data['error']), '')
                if error_detail_keyword:
                    self.error_details = data["error"][error_detail_keyword]
```

Please let me know if you are intersted in opening a PR to improve the error parsing in the library! You would also need to add a unit test [here](https://github.com/googleapis/google-api-python-client/blob/master/tests/test_errors.py#L57).


Resolving patch:
diff --git a/googleapiclient/errors.py b/googleapiclient/errors.py
--- a/googleapiclient/errors.py
+++ b/googleapiclient/errors.py
@@ -51,10 +51,9 @@ def _get_reason(self):
             data = json.loads(self.content.decode("utf-8"))
             if isinstance(data, dict):
                 reason = data["error"]["message"]
-                if "details" in data["error"]:
-                    self.error_details = data["error"]["details"]
-                elif "detail" in data["error"]:
-                    self.error_details = data["error"]["detail"]
+                error_detail_keyword = next((kw for kw in ["detail", "details", "message"] if kw in data["error"]), "")
+                if error_detail_keyword:
+                    self.error_details = data["error"][error_detail_keyword]
             elif isinstance(data, list) and len(data) > 0:
                 first_error = data[0]
                 reason = first_error["error"]["message"]


Test patch:
diff --git a/tests/test_errors.py b/tests/test_errors.py
--- a/tests/test_errors.py
+++ b/tests/test_errors.py
@@ -47,6 +47,24 @@
 }
 """
 
+JSON_ERROR_CONTENT_NO_DETAIL = b"""
+{
+ "error": {
+  "errors": [
+   {
+    "domain": "global",
+    "reason": "required",
+    "message": "country is required",
+    "locationType": "parameter",
+    "location": "country"
+   }
+  ],
+  "code": 400,
+  "message": "country is required"
+ }
+}
+"""
+
 
 def fake_response(data, headers, reason="Ok"):
     response = httplib2.Response(headers)
@@ -112,3 +130,14 @@ def test_missing_reason(self):
         resp, content = fake_response(b"}NOT OK", {"status": "400"}, reason=None)
         error = HttpError(resp, content)
         self.assertEqual(str(error), '<HttpError 400 "">')
+
+    def test_error_detail_for_missing_message_in_error(self):
+        """Test handling of data with missing 'details' or 'detail' element."""
+        resp, content = fake_response(
+            JSON_ERROR_CONTENT_NO_DETAIL,
+            {"status": "400", "content-type": "application/json"},
+            reason="Failed",
+        )
+        error = HttpError(resp, content)
+        self.assertEqual(str(error), '<HttpError 400 when requesting None returned "country is required". Details: "country is required">')
+        self.assertEqual(error.error_details, 'country is required')
diff --git a/tests/test_http.py b/tests/test_http.py
--- a/tests/test_http.py
+++ b/tests/test_http.py
@@ -1567,7 +1567,8 @@ def test_execute_batch_http_error(self):
         expected = (
             "<HttpError 403 when requesting "
             "https://www.googleapis.com/someapi/v1/collection/?foo=bar returned "
-            '"Access Not Configured">'
+            '"Access Not Configured". '
+            'Details: "Access Not Configured">'
         )
         self.assertEqual(expected, str(callbacks.exceptions["2"]))