blob: e98c76db14a32ea79b54f340db3d31d6732b988b [file] [log] [blame]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001# Copyright 2016 Google Inc. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Error handling and exceptions used in the local Cloud Endpoints server."""
16
17# pylint: disable=g-bad-name
18from __future__ import absolute_import
19
20import json
21import logging
22
23from . import generated_error_info
24
25__all__ = ['BackendError',
26 'BasicTypeParameterError',
27 'EnumRejectionError',
28 'InvalidParameterError',
29 'RequestError',
30 'RequestRejectionError']
31
32_logger = logging.getLogger(__name__)
33
34_INVALID_ENUM_TEMPLATE = 'Invalid string value: %r. Allowed values: %r'
35_INVALID_BASIC_PARAM_TEMPLATE = 'Invalid %s value: %r.'
36
37
38class RequestError(Exception):
39 """Base class for errors that happen while processing a request."""
40
41 def status_code(self):
42 """HTTP status code number associated with this error.
43
44 Subclasses must implement this, returning an integer with the status
45 code number for the error.
46
47 Example: 400
48
49 Raises:
50 NotImplementedError: Subclasses must override this function.
51 """
52 raise NotImplementedError
53
54 def message(self):
55 """Text message explaining the error.
56
57 Subclasses must implement this, returning a string that explains the
58 error.
59
60 Raises:
61 NotImplementedError: Subclasses must override this function.
62 """
63 raise NotImplementedError
64
65 def reason(self):
66 """Get the reason for the error.
67
68 Error reason is a custom string in the Cloud Endpoints server. When
69 possible, this should match the reason that the live server will generate,
70 based on the error's status code. If this returns None, the error formatter
71 will attempt to generate a reason from the status code.
72
73 Returns:
74 None, by default. Subclasses can override this if they have a specific
75 error reason.
76 """
77 raise NotImplementedError
78
79 def domain(self):
80 """Get the domain for this error.
81
82 Returns:
83 The string 'global' by default. Subclasses can override this if they have
84 a different domain.
85 """
86 return 'global'
87
88 def extra_fields(self):
89 """Return a dict of extra fields to add to the error response.
90
91 Some errors have additional information. This provides a way for subclasses
92 to provide that information.
93
94 Returns:
95 None, by default. Subclasses can return a dict with values to add
96 to the error response.
97 """
98 return None
99
100 def __format_error(self, error_list_tag):
101 """Format this error into a JSON response.
102
103 Args:
104 error_list_tag: A string specifying the name of the tag to use for the
105 error list.
106
107 Returns:
108 A dict containing the reformatted JSON error response.
109 """
110 error = {'domain': self.domain(),
111 'reason': self.reason(),
112 'message': self.message()}
113 error.update(self.extra_fields() or {})
114 return {'error': {error_list_tag: [error],
115 'code': self.status_code(),
116 'message': self.message()}}
117
118 def rest_error(self):
119 """Format this error into a response to a REST request.
120
121 Returns:
122 A string containing the reformatted error response.
123 """
124 error_json = self.__format_error('errors')
125 return json.dumps(error_json, indent=1, sort_keys=True)
126
127 def rpc_error(self):
128 """Format this error into a response to a JSON RPC request.
129
130
131 Returns:
132 A dict containing the reformatted JSON error response.
133 """
134 return self.__format_error('data')
135
136
137class RequestRejectionError(RequestError):
138 """Base class for invalid/rejected requests.
139
140 To be raised when parsing the request values and comparing them against the
141 generated discovery document.
142 """
143
144 def status_code(self):
145 return 400
146
147
148class InvalidParameterError(RequestRejectionError):
149 """Base class for invalid parameter errors.
150
151 Child classes only need to implement the message() function.
152 """
153
154 def __init__(self, parameter_name, value):
155 """Constructor for InvalidParameterError.
156
157 Args:
158 parameter_name: String; the name of the parameter which had a value
159 rejected.
160 value: The actual value passed in for the parameter. Usually string.
161 """
162 super(InvalidParameterError, self).__init__()
163 self.parameter_name = parameter_name
164 self.value = value
165
166 def reason(self):
167 """Returns the server's reason for this error.
168
169 Returns:
170 A string containing a short error reason.
171 """
172 return 'invalidParameter'
173
174 def extra_fields(self):
175 """Returns extra fields to add to the error response.
176
177 Returns:
178 A dict containing extra fields to add to the error response.
179 """
180 return {'locationType': 'parameter',
181 'location': self.parameter_name}
182
183
184class BasicTypeParameterError(InvalidParameterError):
185 """Request rejection exception for basic types (int, float)."""
186
187 def __init__(self, parameter_name, value, type_name):
188 """Constructor for BasicTypeParameterError.
189
190 Args:
191 parameter_name: String; the name of the parameter which had a value
192 rejected.
193 value: The actual value passed in for the enum. Usually string.
194 type_name: Descriptive name of the data type expected.
195 """
196 super(BasicTypeParameterError, self).__init__(parameter_name, value)
197 self.type_name = type_name
198
199 def message(self):
200 """A descriptive message describing the error."""
201 return _INVALID_BASIC_PARAM_TEMPLATE % (self.type_name, self.value)
202
203
204class EnumRejectionError(InvalidParameterError):
205 """Custom request rejection exception for enum values."""
206
207 def __init__(self, parameter_name, value, allowed_values):
208 """Constructor for EnumRejectionError.
209
210 Args:
211 parameter_name: String; the name of the enum parameter which had a value
212 rejected.
213 value: The actual value passed in for the enum. Usually string.
214 allowed_values: List of strings allowed for the enum.
215 """
216 super(EnumRejectionError, self).__init__(parameter_name, value)
217 self.allowed_values = allowed_values
218
219 def message(self):
220 """A descriptive message describing the error."""
221 return _INVALID_ENUM_TEMPLATE % (self.value, self.allowed_values)
222
223
224class BackendError(RequestError):
225 """Exception raised when the backend returns an error code."""
226
227 def __init__(self, body, status):
228 super(BackendError, self).__init__()
229 # Convert backend error status to whatever the live server would return.
230 status_code = self._get_status_code(status)
231 self._error_info = generated_error_info.get_error_info(status_code)
232
233 try:
234 error_json = json.loads(body)
235 self._message = error_json.get('error_message')
236 except TypeError:
237 self._message = body
238
239 def _get_status_code(self, http_status):
240 """Get the HTTP status code from an HTTP status string.
241
242 Args:
243 http_status: A string containing a HTTP status code and reason.
244
245 Returns:
246 An integer with the status code number from http_status.
247 """
248 try:
249 return int(http_status.split(' ', 1)[0])
250 except TypeError:
251 _logger.warning('Unable to find status code in HTTP status %r.',
252 http_status)
253 return 500
254
255 def status_code(self):
256 """Return the HTTP status code number for this error.
257
258 Returns:
259 An integer containing the status code for this error.
260 """
261 return self._error_info.http_status
262
263 def message(self):
264 """Return a descriptive message for this error.
265
266 Returns:
267 A string containing a descriptive message for this error.
268 """
269 return self._message
270
271 def reason(self):
272 """Return the short reason for this error.
273
274 Returns:
275 A string with the reason for this error.
276 """
277 return self._error_info.reason
278
279 def domain(self):
280 """Return the remapped domain for this error.
281
282 Returns:
283 A string containing the remapped domain for this error.
284 """
285 return self._error_info.domain