Repository: line/line-bot-sdk-python
Pull request #218 (2019-10-26 03:57:18)

Update LineBotApiError to get X-Line-Request-Id
`LineBotApiError` should return `X-Line-Request-Id` which helps developers track errors.


Patch:
diff --git a/README.rst b/README.rst
index a6bb727..d10bbdc 100644
--- a/README.rst
+++ b/README.rst
@@ -521,6 +521,7 @@ https://developers.line.biz/en/reference/messaging-api/#error-responses
         line_bot_api.push_message('to', TextSendMessage(text='Hello World!'))
     except linebot.exceptions.LineBotApiError as e:
         print(e.status_code)
+        print(e.request_id)
         print(e.error.message)
         print(e.error.details)
 
diff --git a/linebot/api.py b/linebot/api.py
index 0bcfa9c..689dd88 100644
--- a/linebot/api.py
+++ b/linebot/api.py
@@ -1008,5 +1008,9 @@ class LineBotApi(object):
         if 200 <= response.status_code < 300:
             pass
         else:
-            error = Error.new_from_json_dict(response.json)
-            raise LineBotApiError(response.status_code, error)
+            raise LineBotApiError(
+                status_code=response.status_code,
+                headers=dict(response.headers.items()),
+                request_id=response.headers.get('X-Line-Request-Id'),
+                error=Error.new_from_json_dict(response.json)
+            )
diff --git a/linebot/exceptions.py b/linebot/exceptions.py
index 9f28c35..c66615a 100644
--- a/linebot/exceptions.py
+++ b/linebot/exceptions.py
@@ -58,16 +58,21 @@ class InvalidSignatureError(BaseError):
 class LineBotApiError(BaseError):
     """When LINE Messaging API response error, this error will be raised."""
 
-    def __init__(self, status_code, error=None):
+    def __init__(self, status_code, headers, request_id=None, error=None):
         """__init__ method.
 
-        :param int status_code: http status code
+        :param int status_code: HTTP status code
+        :param headers: Response headers
+        :type headers: dict[str, str]
+        :param str request_id: (optional) Request ID. A unique ID is generated for each request
         :param error: (optional) Error class object.
         :type error: :py:class:`linebot.models.error.Error`
         """
         super(LineBotApiError, self).__init__(error.message)
 
         self.status_code = status_code
+        self.headers = headers
+        self.request_id = request_id
         self.error = error
 
     def __str__(self):
@@ -75,5 +80,5 @@ class LineBotApiError(BaseError):
 
         :rtype: str
         """
-        return '{0}: status_code={1}, error_response={2}'.format(
-            self.__class__.__name__, self.status_code, self.error)
+        return '{0}: status_code={1}, request_id={2}, error_response={3}, headers={4}'.format(
+            self.__class__.__name__, self.status_code, self.request_id, self.error, self.headers)
diff --git a/linebot/http_client.py b/linebot/http_client.py
index d72ed1a..b3e0568 100644
--- a/linebot/http_client.py
+++ b/linebot/http_client.py
@@ -233,7 +233,10 @@ class RequestsHttpResponse(HttpResponse):
 
     @property
     def headers(self):
-        """Get headers."""
+        """Get headers.
+
+        :rtype :py:class:`requests.models.CaseInsensitiveDict`
+        """
         return self.response.headers
 
     @property


Test patch:
diff --git a/tests/api/test_error_handle.py b/tests/api/test_error_handle.py
index 2afa90b..99e74ca 100644
--- a/tests/api/test_error_handle.py
+++ b/tests/api/test_error_handle.py
@@ -17,6 +17,7 @@ from __future__ import unicode_literals, absolute_import
 import unittest
 
 import responses
+
 from linebot import (
     LineBotApi
 )
@@ -31,6 +32,8 @@ from linebot.models import (
 class TestLineBotApi(unittest.TestCase):
     def setUp(self):
         self.tested = LineBotApi('channel_secret')
+        self.request_id = 'f70dd685-499a-4231-a441-f24b8d4fba21'
+        self.headers = {'X-Line-Request-Id': self.request_id, 'HOGE': 'FUGA'}
 
     @responses.activate
     def test_error_handle(self):
@@ -40,6 +43,7 @@ class TestLineBotApi(unittest.TestCase):
             json={
                 "message": "Invalid reply token"
             },
+            headers=self.headers,
             status=401
         )
 
@@ -48,6 +52,8 @@ class TestLineBotApi(unittest.TestCase):
         except LineBotApiError as e:
             self.assertEqual(e.status_code, 401)
             self.assertEqual(e.error.message, 'Invalid reply token')
+            self.assertEqual(e.request_id, self.request_id)
+            self.assertEqual(e.headers['HOGE'], 'FUGA')
 
     @responses.activate
     def test_error_with_detail_handle(self):
@@ -69,6 +75,7 @@ class TestLineBotApi(unittest.TestCase):
                     }
                 ]
             },
+            headers=self.headers,
             status=400
         )
 
@@ -90,6 +97,8 @@ class TestLineBotApi(unittest.TestCase):
                                             "richmessage, template, imagemap]"
             )
             self.assertEqual(e.error.details[1].property, 'messages[1].type')
+            self.assertEqual(e.request_id, self.request_id)
+            self.assertEqual(e.headers['HOGE'], 'FUGA')
 
     @responses.activate
     def test_error_handle_get_message_content(self):
@@ -99,6 +108,7 @@ class TestLineBotApi(unittest.TestCase):
             json={
                 "message": "Invalid reply token"
             },
+            headers=self.headers,
             status=404
         )
 
@@ -107,6 +117,8 @@ class TestLineBotApi(unittest.TestCase):
         except LineBotApiError as e:
             self.assertEqual(e.status_code, 404)
             self.assertEqual(e.error.message, 'Invalid reply token')
+            self.assertEqual(e.request_id, self.request_id)
+            self.assertEqual(e.headers['HOGE'], 'FUGA')
 
 
 if __name__ == '__main__':
diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py
index 61a37ee..07abb25 100644
--- a/tests/test_exceptions.py
+++ b/tests/test_exceptions.py
@@ -20,17 +20,25 @@ from linebot.exceptions import LineBotApiError
 from linebot.models import Error, ErrorDetail
 
 
-class TestUtils(unittest.TestCase):
+class TestExceptions(unittest.TestCase):
+    maxDiff = None
+
     def test_str(self):
+        headers = {'X-Line-Request-Id': 'f70dd685-499a-4231-a441-f24b8d4fba21'}
         line_bot_api_error = LineBotApiError(
-            400,
+            status_code=400,
+            request_id='f70dd685-499a-4231-a441-f24b8d4fba21',
+            headers=headers,
             error=Error(message='The request body has 1 error(s)',
                         details=[ErrorDetail(message='May not be empty',
                                              property='messages[0].text')]))
-        self.assertEqual(line_bot_api_error.__str__(),
-                         'LineBotApiError: status_code=400, error_response={"details": '
-                         '[{"message": "May not be empty", "property": "messages[0].text"}], '
-                         '"message": "The request body has 1 error(s)"}')
+        self.assertEqual(
+            line_bot_api_error.__str__(),
+            'LineBotApiError: status_code=400, request_id=f70dd685-499a-4231-a441-f24b8d4fba21, '
+            'error_response={"details": [{"message": "May not be empty", '
+            '"property": "messages[0].text"}], "message": "The request body has 1 error(s)"}, '
+            + 'headers={}'.format(headers)
+        )
 
 
 if __name__ == '__main__':