14
14
15
15
"""Internal utilities common to all modules."""
16
16
17
+ import json
18
+ import socket
19
+
20
+ import googleapiclient
21
+ import httplib2
17
22
import requests
23
+ import six
18
24
19
25
import firebase_admin
20
26
from firebase_admin import exceptions
21
27
22
28
23
- _STATUS_TO_EXCEPTION_TYPE = {
24
- 400 : exceptions .InvalidArgumentError ,
25
- 401 : exceptions .UnauthenticatedError ,
26
- 403 : exceptions .PermissionDeniedError ,
27
- 404 : exceptions .NotFoundError ,
28
- 409 : exceptions .ConflictError ,
29
- 429 : exceptions .ResourceExhaustedError ,
30
- 500 : exceptions .InternalError ,
31
- 503 : exceptions .UnavailableError ,
29
+ _ERROR_CODE_TO_EXCEPTION_TYPE = {
30
+ exceptions .INVALID_ARGUMENT : exceptions .InvalidArgumentError ,
31
+ exceptions .FAILED_PRECONDITION : exceptions .FailedPreconditionError ,
32
+ exceptions .OUT_OF_RANGE : exceptions .OutOfRangeError ,
33
+ exceptions .UNAUTHENTICATED : exceptions .UnauthenticatedError ,
34
+ exceptions .PERMISSION_DENIED : exceptions .PermissionDeniedError ,
35
+ exceptions .NOT_FOUND : exceptions .NotFoundError ,
36
+ exceptions .ABORTED : exceptions .AbortedError ,
37
+ exceptions .ALREADY_EXISTS : exceptions .AlreadyExistsError ,
38
+ exceptions .CONFLICT : exceptions .ConflictError ,
39
+ exceptions .RESOURCE_EXHAUSTED : exceptions .ResourceExhaustedError ,
40
+ exceptions .CANCELLED : exceptions .CancelledError ,
41
+ exceptions .DATA_LOSS : exceptions .DataLossError ,
42
+ exceptions .UNKNOWN : exceptions .UnknownError ,
43
+ exceptions .INTERNAL : exceptions .InternalError ,
44
+ exceptions .UNAVAILABLE : exceptions .UnavailableError ,
45
+ exceptions .DEADLINE_EXCEEDED : exceptions .DeadlineExceededError ,
46
+ }
47
+
48
+
49
+ _HTTP_STATUS_TO_ERROR_CODE = {
50
+ 400 : exceptions .INVALID_ARGUMENT ,
51
+ 401 : exceptions .UNAUTHENTICATED ,
52
+ 403 : exceptions .PERMISSION_DENIED ,
53
+ 404 : exceptions .NOT_FOUND ,
54
+ 409 : exceptions .CONFLICT ,
55
+ 429 : exceptions .RESOURCE_EXHAUSTED ,
56
+ 500 : exceptions .INTERNAL ,
57
+ 503 : exceptions .UNAVAILABLE ,
32
58
}
33
59
34
60
@@ -45,19 +71,69 @@ def _get_initialized_app(app):
45
71
raise ValueError ('Illegal app argument. Argument must be of type '
46
72
' firebase_admin.App, but given "{0}".' .format (type (app )))
47
73
74
+
48
75
def get_app_service (app , name , initializer ):
49
76
app = _get_initialized_app (app )
50
77
return app ._get_service (name , initializer ) # pylint: disable=protected-access
51
78
52
- def handle_requests_error (error , message = None , status = None ):
79
+
80
+ def handle_platform_error_from_requests (error , handle_func = None ):
81
+ """Constructs a ``FirebaseError`` from the given requests error.
82
+
83
+ This can be used to handle errors returned by Google Cloud Platform (GCP) APIs.
84
+
85
+ Args:
86
+ error: An error raised by the requests module while making an HTTP call to a GCP API.
87
+ handle_func: A function that can be used to handle platform errors in a custom way. When
88
+ specified, this function will be called with three arguments. It has the same
89
+ signature as ```_handle_func_requests``, but may return ``None``.
90
+
91
+ Returns:
92
+ FirebaseError: A ``FirebaseError`` that can be raised to the user code.
93
+ """
94
+ if error .response is None :
95
+ return handle_requests_error (error )
96
+
97
+ response = error .response
98
+ content = response .content .decode ()
99
+ status_code = response .status_code
100
+ error_dict , message = _parse_platform_error (content , status_code )
101
+ exc = None
102
+ if handle_func :
103
+ exc = handle_func (error , message , error_dict )
104
+
105
+ return exc if exc else _handle_func_requests (error , message , error_dict )
106
+
107
+
108
+ def _handle_func_requests (error , message , error_dict ):
109
+ """Constructs a ``FirebaseError`` from the given GCP error.
110
+
111
+ Args:
112
+ error: An error raised by the requests module while making an HTTP call.
113
+ message: A message to be included in the resulting ``FirebaseError``.
114
+ error_dict: Parsed GCP error response.
115
+
116
+ Returns:
117
+ FirebaseError: A ``FirebaseError`` that can be raised to the user code or None.
118
+ """
119
+ code = error_dict .get ('status' )
120
+ return handle_requests_error (error , message , code )
121
+
122
+
123
+ def handle_requests_error (error , message = None , code = None ):
53
124
"""Constructs a ``FirebaseError`` from the given requests error.
54
125
126
+ This method is agnostic of the remote service that produced the error, whether it is a GCP
127
+ service or otherwise. Therefore, this method does not attempt to parse the error response in
128
+ any way.
129
+
55
130
Args:
56
- error: An error raised by the reqests module while making an HTTP call.
131
+ error: An error raised by the requests module while making an HTTP call.
57
132
message: A message to be included in the resulting ``FirebaseError`` (optional). If not
58
133
specified the string representation of the ``error`` argument is used as the message.
59
- status: An HTTP status code that will be used to determine the resulting error type
60
- (optional). If not specified the HTTP status code on the error response is used.
134
+ code: A GCP error code that will be used to determine the resulting error type (optional).
135
+ If not specified the HTTP status code on the error response is used to determine a
136
+ suitable error code.
61
137
62
138
Returns:
63
139
FirebaseError: A ``FirebaseError`` that can be raised to the user code.
@@ -75,9 +151,143 @@ def handle_requests_error(error, message=None, status=None):
75
151
message = 'Unknown error while making a remote service call: {0}' .format (error ),
76
152
cause = error )
77
153
78
- if not status :
79
- status = error .response .status_code
154
+ if not code :
155
+ code = _http_status_to_error_code ( error .response .status_code )
80
156
if not message :
81
157
message = str (error )
82
- err_type = _STATUS_TO_EXCEPTION_TYPE .get (status , exceptions .UnknownError )
158
+
159
+ err_type = _error_code_to_exception_type (code )
83
160
return err_type (message = message , cause = error , http_response = error .response )
161
+
162
+
163
+ def handle_platform_error_from_googleapiclient (error , handle_func = None ):
164
+ """Constructs a ``FirebaseError`` from the given googleapiclient error.
165
+
166
+ This can be used to handle errors returned by Google Cloud Platform (GCP) APIs.
167
+
168
+ Args:
169
+ error: An error raised by the googleapiclient while making an HTTP call to a GCP API.
170
+ handle_func: A function that can be used to handle platform errors in a custom way. When
171
+ specified, this function will be called with three arguments. It has the same
172
+ signature as ```_handle_func_googleapiclient``, but may return ``None``.
173
+
174
+ Returns:
175
+ FirebaseError: A ``FirebaseError`` that can be raised to the user code.
176
+ """
177
+ if not isinstance (error , googleapiclient .errors .HttpError ):
178
+ return handle_googleapiclient_error (error )
179
+
180
+ content = error .content .decode ()
181
+ status_code = error .resp .status
182
+ error_dict , message = _parse_platform_error (content , status_code )
183
+ http_response = _http_response_from_googleapiclient_error (error )
184
+ exc = None
185
+ if handle_func :
186
+ exc = handle_func (error , message , error_dict , http_response )
187
+
188
+ return exc if exc else _handle_func_googleapiclient (error , message , error_dict , http_response )
189
+
190
+
191
+ def _handle_func_googleapiclient (error , message , error_dict , http_response ):
192
+ """Constructs a ``FirebaseError`` from the given GCP error.
193
+
194
+ Args:
195
+ error: An error raised by the googleapiclient module while making an HTTP call.
196
+ message: A message to be included in the resulting ``FirebaseError``.
197
+ error_dict: Parsed GCP error response.
198
+ http_response: A requests HTTP response object to associate with the exception.
199
+
200
+ Returns:
201
+ FirebaseError: A ``FirebaseError`` that can be raised to the user code or None.
202
+ """
203
+ code = error_dict .get ('status' )
204
+ return handle_googleapiclient_error (error , message , code , http_response )
205
+
206
+
207
+ def handle_googleapiclient_error (error , message = None , code = None , http_response = None ):
208
+ """Constructs a ``FirebaseError`` from the given googleapiclient error.
209
+
210
+ This method is agnostic of the remote service that produced the error, whether it is a GCP
211
+ service or otherwise. Therefore, this method does not attempt to parse the error response in
212
+ any way.
213
+
214
+ Args:
215
+ error: An error raised by the googleapiclient module while making an HTTP call.
216
+ message: A message to be included in the resulting ``FirebaseError`` (optional). If not
217
+ specified the string representation of the ``error`` argument is used as the message.
218
+ code: A GCP error code that will be used to determine the resulting error type (optional).
219
+ If not specified the HTTP status code on the error response is used to determine a
220
+ suitable error code.
221
+ http_response: A requests HTTP response object to associate with the exception (optional).
222
+ If not specified, one will be created from the ``error``.
223
+
224
+ Returns:
225
+ FirebaseError: A ``FirebaseError`` that can be raised to the user code.
226
+ """
227
+ if isinstance (error , socket .timeout ) or (
228
+ isinstance (error , socket .error ) and 'timed out' in str (error )):
229
+ return exceptions .DeadlineExceededError (
230
+ message = 'Timed out while making an API call: {0}' .format (error ),
231
+ cause = error )
232
+ elif isinstance (error , httplib2 .ServerNotFoundError ):
233
+ return exceptions .UnavailableError (
234
+ message = 'Failed to establish a connection: {0}' .format (error ),
235
+ cause = error )
236
+ elif not isinstance (error , googleapiclient .errors .HttpError ):
237
+ return exceptions .UnknownError (
238
+ message = 'Unknown error while making a remote service call: {0}' .format (error ),
239
+ cause = error )
240
+
241
+ if not code :
242
+ code = _http_status_to_error_code (error .resp .status )
243
+ if not message :
244
+ message = str (error )
245
+ if not http_response :
246
+ http_response = _http_response_from_googleapiclient_error (error )
247
+
248
+ err_type = _error_code_to_exception_type (code )
249
+ return err_type (message = message , cause = error , http_response = http_response )
250
+
251
+
252
+ def _http_response_from_googleapiclient_error (error ):
253
+ """Creates a requests HTTP Response object from the given googleapiclient error."""
254
+ resp = requests .models .Response ()
255
+ resp .raw = six .BytesIO (error .content )
256
+ resp .status_code = error .resp .status
257
+ return resp
258
+
259
+
260
+ def _http_status_to_error_code (status ):
261
+ """Maps an HTTP status to a platform error code."""
262
+ return _HTTP_STATUS_TO_ERROR_CODE .get (status , exceptions .UNKNOWN )
263
+
264
+
265
+ def _error_code_to_exception_type (code ):
266
+ """Maps a platform error code to an exception type."""
267
+ return _ERROR_CODE_TO_EXCEPTION_TYPE .get (code , exceptions .UnknownError )
268
+
269
+
270
+ def _parse_platform_error (content , status_code ):
271
+ """Parses an HTTP error response from a Google Cloud Platform API and extracts the error code
272
+ and message fields.
273
+
274
+ Args:
275
+ content: Decoded content of the response body.
276
+ status_code: HTTP status code.
277
+
278
+ Returns:
279
+ tuple: A tuple containing error code and message.
280
+ """
281
+ data = {}
282
+ try :
283
+ parsed_body = json .loads (content )
284
+ if isinstance (parsed_body , dict ):
285
+ data = parsed_body
286
+ except ValueError :
287
+ pass
288
+
289
+ error_dict = data .get ('error' , {})
290
+ msg = error_dict .get ('message' )
291
+ if not msg :
292
+ msg = 'Unexpected HTTP response with status: {0}; body: {1}' .format (status_code , content )
293
+ return error_dict , msg
0 commit comments