Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(126)

Side by Side Diff: third_party/google-endpoints/endpoints/endpoints_dispatcher.py

Issue 2666783008: Add google-endpoints to third_party/. (Closed)
Patch Set: Created 3 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 # 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 """Dispatcher middleware for Cloud Endpoints API server.
16
17 This middleware does simple transforms on requests that come into the base path
18 and then re-dispatches them to the main backend. It does not do any
19 authentication, quota checking, DoS checking, etc.
20
21 In addition, the middleware loads API configs prior to each call, in case the
22 configuration has changed.
23 """
24
25 # pylint: disable=g-bad-name
26 import cStringIO
27 import httplib
28 import json
29 import logging
30 import re
31 import urlparse
32 import wsgiref
33
34 import api_config_manager
35 import api_request
36 import discovery_api_proxy
37 import discovery_service
38 import errors
39 import parameter_converter
40 import util
41
42
43 __all__ = ['EndpointsDispatcherMiddleware']
44
45 _SERVER_SOURCE_IP = '0.2.0.3'
46
47 # Internal constants
48 _CORS_HEADER_ORIGIN = 'Origin'
49 _CORS_HEADER_REQUEST_METHOD = 'Access-Control-Request-Method'
50 _CORS_HEADER_REQUEST_HEADERS = 'Access-Control-Request-Headers'
51 _CORS_HEADER_ALLOW_ORIGIN = 'Access-Control-Allow-Origin'
52 _CORS_HEADER_ALLOW_METHODS = 'Access-Control-Allow-Methods'
53 _CORS_HEADER_ALLOW_HEADERS = 'Access-Control-Allow-Headers'
54 _CORS_HEADER_ALLOW_CREDS = 'Access-Control-Allow-Credentials'
55 _CORS_HEADER_EXPOSE_HEADERS = 'Access-Control-Expose-Headers'
56 _CORS_ALLOWED_METHODS = frozenset(('DELETE', 'GET', 'PATCH', 'POST', 'PUT'))
57 _CORS_EXPOSED_HEADERS = frozenset(
58 ('Content-Encoding', 'Content-Length', 'Date', 'ETag', 'Server')
59 )
60
61
62 class EndpointsDispatcherMiddleware(object):
63 """Dispatcher that handles requests to the built-in apiserver handlers."""
64
65 _API_EXPLORER_URL = 'https://apis-explorer.appspot.com/apis-explorer/?base='
66
67 def __init__(self, backend_wsgi_app, config_manager=None):
68 """Constructor for EndpointsDispatcherMiddleware.
69
70 Args:
71 backend_wsgi_app: A WSGI server that serves the app's endpoints.
72 config_manager: An ApiConfigManager instance that allows a caller to
73 set up an existing configuration for testing.
74 """
75 if config_manager is None:
76 config_manager = api_config_manager.ApiConfigManager()
77 self.config_manager = config_manager
78
79 self._backend = backend_wsgi_app
80 self._dispatchers = []
81 for base_path in self._backend.base_paths:
82 self._add_dispatcher('%sexplorer/?$' % base_path,
83 self.handle_api_explorer_request)
84 self._add_dispatcher('%sstatic/.*$' % base_path,
85 self.handle_api_static_request)
86
87 def _add_dispatcher(self, path_regex, dispatch_function):
88 """Add a request path and dispatch handler.
89
90 Args:
91 path_regex: A string regex, the path to match against incoming requests.
92 dispatch_function: The function to call for these requests. The function
93 should take (request, start_response) as arguments and
94 return the contents of the response body.
95 """
96 self._dispatchers.append((re.compile(path_regex), dispatch_function))
97
98 def __call__(self, environ, start_response):
99 """Handle an incoming request.
100
101 Args:
102 environ: An environ dict for the request as defined in PEP-333.
103 start_response: A function used to begin the response to the caller.
104 This follows the semantics defined in PEP-333. In particular, it's
105 called with (status, response_headers, exc_info=None), and it returns
106 an object with a write(body_data) function that can be used to write
107 the body of the response.
108
109 Yields:
110 An iterable over strings containing the body of the HTTP response.
111 """
112 request = api_request.ApiRequest(environ,
113 base_paths=self._backend.base_paths)
114
115 # PEP-333 requires that we return an iterator that iterates over the
116 # response body. Yielding the returned body accomplishes this.
117 yield self.dispatch(request, start_response)
118
119 def dispatch(self, request, start_response):
120 """Handles dispatch to apiserver handlers.
121
122 This typically ends up calling start_response and returning the entire
123 body of the response.
124
125 Args:
126 request: An ApiRequest, the request from the user.
127 start_response: A function with semantics defined in PEP-333.
128
129 Returns:
130 A string, the body of the response.
131 """
132 # Check if this matches any of our special handlers.
133 dispatched_response = self.dispatch_non_api_requests(request,
134 start_response)
135 if dispatched_response is not None:
136 return dispatched_response
137
138 # Get API configuration first. We need this so we know how to
139 # call the back end.
140 api_config_response = self.get_api_configs()
141 if api_config_response:
142 self.config_manager.process_api_config_response(api_config_response)
143 else:
144 return self.fail_request(request, 'get_api_configs Error',
145 start_response)
146
147 # Call the service.
148 try:
149 return self.call_backend(request, start_response)
150 except errors.RequestError as error:
151 return self._handle_request_error(request, error, start_response)
152
153 def dispatch_non_api_requests(self, request, start_response):
154 """Dispatch this request if this is a request to a reserved URL.
155
156 If the request matches one of our reserved URLs, this calls
157 start_response and returns the response body. This also handles OPTIONS
158 CORS requests.
159
160 Args:
161 request: An ApiRequest, the request from the user.
162 start_response: A function with semantics defined in PEP-333.
163
164 Returns:
165 None if the request doesn't match one of the reserved URLs this
166 handles. Otherwise, returns the response body.
167 """
168 for path_regex, dispatch_function in self._dispatchers:
169 if path_regex.match(request.relative_url):
170 return dispatch_function(request, start_response)
171
172 if request.http_method == 'OPTIONS':
173 cors_handler = self._create_cors_handler(request)
174 if cors_handler.allow_cors_request:
175 # The server returns 200 rather than 204, for some reason.
176 return util.send_wsgi_response('200', [], '', start_response,
177 cors_handler)
178
179 return None
180
181 def handle_api_explorer_request(self, request, start_response):
182 """Handler for requests to {base_path}/explorer.
183
184 This calls start_response and returns the response body.
185
186 Args:
187 request: An ApiRequest, the request from the user.
188 start_response: A function with semantics defined in PEP-333.
189
190 Returns:
191 A string containing the response body (which is empty, in this case).
192 """
193 protocol = 'http' if 'localhost' in request.server else 'https'
194 base_path = request.base_path.strip('/')
195 base_url = '{0}://{1}:{2}/{3}'.format(
196 protocol, request.server, request.port, base_path)
197 redirect_url = self._API_EXPLORER_URL + base_url
198 return util.send_wsgi_redirect_response(redirect_url, start_response)
199
200 def handle_api_static_request(self, request, start_response):
201 """Handler for requests to {base_path}/static/.*.
202
203 This calls start_response and returns the response body.
204
205 Args:
206 request: An ApiRequest, the request from the user.
207 start_response: A function with semantics defined in PEP-333.
208
209 Returns:
210 A string containing the response body.
211 """
212 discovery_api = discovery_api_proxy.DiscoveryApiProxy()
213 response, body = discovery_api.get_static_file(request.relative_url)
214 status_string = '%d %s' % (response.status, response.reason)
215 if response.status == 200:
216 # Some of the headers that come back from the server can't be passed
217 # along in our response. Specifically, the response from the server has
218 # transfer-encoding: chunked, which doesn't apply to the response that
219 # we're forwarding. There may be other problematic headers, so we strip
220 # off everything but Content-Type.
221 return util.send_wsgi_response(status_string,
222 [('Content-Type',
223 response.getheader('Content-Type'))],
224 body, start_response)
225 else:
226 logging.error('Discovery API proxy failed on %s with %d. Details: %s',
227 request.relative_url, response.status, body)
228 return util.send_wsgi_response(status_string, response.getheaders(), body,
229 start_response)
230
231 def get_api_configs(self):
232 return self._backend.get_api_configs()
233
234 @staticmethod
235 def verify_response(response, status_code, content_type=None):
236 """Verifies that a response has the expected status and content type.
237
238 Args:
239 response: The ResponseTuple to be checked.
240 status_code: An int, the HTTP status code to be compared with response
241 status.
242 content_type: A string with the acceptable Content-Type header value.
243 None allows any content type.
244
245 Returns:
246 True if both status_code and content_type match, else False.
247 """
248 status = int(response.status.split(' ', 1)[0])
249 if status != status_code:
250 return False
251
252 if content_type is None:
253 return True
254
255 for header, value in response.headers:
256 if header.lower() == 'content-type':
257 return value == content_type
258
259 # If we fall through to here, the verification has failed, so return False.
260 return False
261
262 def prepare_backend_environ(self, host, method, relative_url, headers, body,
263 source_ip, port):
264 """Build an environ object for the backend to consume.
265
266 Args:
267 host: A string containing the host serving the request.
268 method: A string containing the HTTP method of the request.
269 relative_url: A string containing path and query string of the request.
270 headers: A list of (key, value) tuples where key and value are both
271 strings.
272 body: A string containing the request body.
273 source_ip: The source IP address for the request.
274 port: The port to which to direct the request.
275
276 Returns:
277 An environ object with all the information necessary for the backend to
278 process the request.
279 """
280 if isinstance(body, unicode):
281 body = body.encode('ascii')
282
283 url = urlparse.urlsplit(relative_url)
284 if port != 80:
285 host = '%s:%s' % (host, port)
286 else:
287 host = host
288 environ = {'CONTENT_LENGTH': str(len(body)),
289 'PATH_INFO': url.path,
290 'QUERY_STRING': url.query,
291 'REQUEST_METHOD': method,
292 'REMOTE_ADDR': source_ip,
293 'SERVER_NAME': host,
294 'SERVER_PORT': str(port),
295 'SERVER_PROTOCOL': 'HTTP/1.1',
296 'wsgi.version': (1, 0),
297 'wsgi.url_scheme': 'http',
298 'wsgi.errors': cStringIO.StringIO(),
299 'wsgi.multithread': True,
300 'wsgi.multiprocess': True,
301 'wsgi.input': cStringIO.StringIO(body)}
302 util.put_headers_in_environ(headers, environ)
303 environ['HTTP_HOST'] = host
304 return environ
305
306 def call_backend(self, orig_request, start_response):
307 """Generate API call (from earlier-saved request).
308
309 This calls start_response and returns the response body.
310
311 Args:
312 orig_request: An ApiRequest, the original request from the user.
313 start_response: A function with semantics defined in PEP-333.
314
315 Returns:
316 A string containing the response body.
317 """
318 if orig_request.is_rpc():
319 method_config = self.lookup_rpc_method(orig_request)
320 params = None
321 else:
322 method_config, params = self.lookup_rest_method(orig_request)
323 if not method_config:
324 cors_handler = self._create_cors_handler(orig_request)
325 return util.send_wsgi_not_found_response(start_response,
326 cors_handler=cors_handler)
327
328 # Prepare the request for the back end.
329 transformed_request = self.transform_request(
330 orig_request, params, method_config)
331
332 # Check if this call is for the Discovery service. If so, route
333 # it to our Discovery handler.
334 discovery = discovery_service.DiscoveryService(
335 self.config_manager, self._backend)
336 discovery_response = discovery.handle_discovery_request(
337 transformed_request.path, transformed_request, start_response)
338 if discovery_response:
339 return discovery_response
340
341 url = transformed_request.base_path + transformed_request.path
342 transformed_request.headers['Content-Type'] = 'application/json'
343 transformed_environ = self.prepare_backend_environ(
344 orig_request.server, 'POST', url, transformed_request.headers.items(),
345 transformed_request.body, transformed_request.source_ip,
346 orig_request.port)
347
348 # Send the transformed request to the backend app and capture the response.
349 with util.StartResponseProxy() as start_response_proxy:
350 body_iter = self._backend(transformed_environ, start_response_proxy.Proxy)
351 status = start_response_proxy.response_status
352 headers = start_response_proxy.response_headers
353
354 # Get response body
355 body = start_response_proxy.response_body
356 # In case standard WSGI behavior is implemented later...
357 if not body:
358 body = ''.join(body_iter)
359
360 return self.handle_backend_response(orig_request, transformed_request,
361 status, headers, body, method_config,
362 start_response)
363
364 class __CheckCorsHeaders(object):
365 """Track information about CORS headers and our response to them."""
366
367 def __init__(self, request):
368 self.allow_cors_request = False
369 self.origin = None
370 self.cors_request_method = None
371 self.cors_request_headers = None
372
373 self.__check_cors_request(request)
374
375 def __check_cors_request(self, request):
376 """Check for a CORS request, and see if it gets a CORS response."""
377 # Check for incoming CORS headers.
378 self.origin = request.headers[_CORS_HEADER_ORIGIN]
379 self.cors_request_method = request.headers[_CORS_HEADER_REQUEST_METHOD]
380 self.cors_request_headers = request.headers[
381 _CORS_HEADER_REQUEST_HEADERS]
382
383 # Check if the request should get a CORS response.
384 if (self.origin and
385 ((self.cors_request_method is None) or
386 (self.cors_request_method.upper() in _CORS_ALLOWED_METHODS))):
387 self.allow_cors_request = True
388
389 def update_headers(self, headers_in):
390 """Add CORS headers to the response, if needed."""
391 if not self.allow_cors_request:
392 return
393
394 # Add CORS headers.
395 headers = wsgiref.headers.Headers(headers_in)
396 headers[_CORS_HEADER_ALLOW_CREDS] = 'true'
397 headers[_CORS_HEADER_ALLOW_ORIGIN] = self.origin
398 headers[_CORS_HEADER_ALLOW_METHODS] = ','.join(tuple(
399 _CORS_ALLOWED_METHODS))
400 headers[_CORS_HEADER_EXPOSE_HEADERS] = ','.join(tuple(
401 _CORS_EXPOSED_HEADERS))
402 if self.cors_request_headers is not None:
403 headers[_CORS_HEADER_ALLOW_HEADERS] = self.cors_request_headers
404
405 def _create_cors_handler(self, request):
406 return EndpointsDispatcherMiddleware.__CheckCorsHeaders(request)
407
408 def handle_backend_response(self, orig_request, backend_request,
409 response_status, response_headers,
410 response_body, method_config, start_response):
411 """Handle backend response, transforming output as needed.
412
413 This calls start_response and returns the response body.
414
415 Args:
416 orig_request: An ApiRequest, the original request from the user.
417 backend_request: An ApiRequest, the transformed request that was
418 sent to the backend handler.
419 response_status: A string, the status from the response.
420 response_headers: A dict, the headers from the response.
421 response_body: A string, the body of the response.
422 method_config: A dict, the API config of the method to be called.
423 start_response: A function with semantics defined in PEP-333.
424
425 Returns:
426 A string containing the response body.
427 """
428 # Verify that the response is json. If it isn't treat, the body as an
429 # error message and wrap it in a json error response.
430 for header, value in response_headers:
431 if (header.lower() == 'content-type' and
432 not value.lower().startswith('application/json')):
433 return self.fail_request(orig_request,
434 'Non-JSON reply: %s' % response_body,
435 start_response)
436
437 self.check_error_response(response_body, response_status)
438
439 # Need to check is_rpc() against the original request, because the
440 # incoming request here has had its path modified.
441 if orig_request.is_rpc():
442 body = self.transform_jsonrpc_response(backend_request, response_body)
443 else:
444 # Check if the response from the API was empty. Empty REST responses
445 # generate a HTTP 204.
446 empty_response = self.check_empty_response(orig_request, method_config,
447 start_response)
448 if empty_response is not None:
449 return empty_response
450
451 body = self.transform_rest_response(response_body)
452
453 cors_handler = self._create_cors_handler(orig_request)
454 return util.send_wsgi_response(response_status, response_headers, body,
455 start_response, cors_handler=cors_handler)
456
457 def fail_request(self, orig_request, message, start_response):
458 """Write an immediate failure response to outfile, no redirect.
459
460 This calls start_response and returns the error body.
461
462 Args:
463 orig_request: An ApiRequest, the original request from the user.
464 message: A string containing the error message to be displayed to user.
465 start_response: A function with semantics defined in PEP-333.
466
467 Returns:
468 A string containing the body of the error response.
469 """
470 cors_handler = self._create_cors_handler(orig_request)
471 return util.send_wsgi_error_response(
472 message, start_response, cors_handler=cors_handler)
473
474 def lookup_rest_method(self, orig_request):
475 """Looks up and returns rest method for the currently-pending request.
476
477 Args:
478 orig_request: An ApiRequest, the original request from the user.
479
480 Returns:
481 A tuple of (method descriptor, parameters), or (None, None) if no method
482 was found for the current request.
483 """
484 method_name, method, params = self.config_manager.lookup_rest_method(
485 orig_request.path, orig_request.http_method)
486 orig_request.method_name = method_name
487 return method, params
488
489 def lookup_rpc_method(self, orig_request):
490 """Looks up and returns RPC method for the currently-pending request.
491
492 Args:
493 orig_request: An ApiRequest, the original request from the user.
494
495 Returns:
496 The RPC method descriptor that was found for the current request, or None
497 if none was found.
498 """
499 if not orig_request.body_json:
500 return None
501 method_name = orig_request.body_json.get('method', '')
502 version = orig_request.body_json.get('apiVersion', '')
503 orig_request.method_name = method_name
504 return self.config_manager.lookup_rpc_method(method_name, version)
505
506 def transform_request(self, orig_request, params, method_config):
507 """Transforms orig_request to apiserving request.
508
509 This method uses orig_request to determine the currently-pending request
510 and returns a new transformed request ready to send to the backend. This
511 method accepts a rest-style or RPC-style request.
512
513 Args:
514 orig_request: An ApiRequest, the original request from the user.
515 params: A dictionary containing path parameters for rest requests, or
516 None for an RPC request.
517 method_config: A dict, the API config of the method to be called.
518
519 Returns:
520 An ApiRequest that's a copy of the current request, modified so it can
521 be sent to the backend. The path is updated and parts of the body or
522 other properties may also be changed.
523 """
524 if orig_request.is_rpc():
525 request = self.transform_jsonrpc_request(orig_request)
526 else:
527 method_params = method_config.get('request', {}).get('parameters', {})
528 request = self.transform_rest_request(orig_request, params, method_params)
529 request.path = method_config.get('rosyMethod', '')
530 return request
531
532 def _add_message_field(self, field_name, value, params):
533 """Converts a . delimitied field name to a message field in parameters.
534
535 This adds the field to the params dict, broken out so that message
536 parameters appear as sub-dicts within the outer param.
537
538 For example:
539 {'a.b.c': ['foo']}
540 becomes:
541 {'a': {'b': {'c': ['foo']}}}
542
543 Args:
544 field_name: A string containing the '.' delimitied name to be converted
545 into a dictionary.
546 value: The value to be set.
547 params: The dictionary holding all the parameters, where the value is
548 eventually set.
549 """
550 if '.' not in field_name:
551 params[field_name] = value
552 return
553
554 root, remaining = field_name.split('.', 1)
555 sub_params = params.setdefault(root, {})
556 self._add_message_field(remaining, value, sub_params)
557
558 def _update_from_body(self, destination, source):
559 """Updates the dictionary for an API payload with the request body.
560
561 The values from the body should override those already in the payload, but
562 for nested fields (message objects) the values can be combined
563 recursively.
564
565 Args:
566 destination: A dictionary containing an API payload parsed from the
567 path and query parameters in a request.
568 source: A dictionary parsed from the body of the request.
569 """
570 for key, value in source.iteritems():
571 destination_value = destination.get(key)
572 if isinstance(value, dict) and isinstance(destination_value, dict):
573 self._update_from_body(destination_value, value)
574 else:
575 destination[key] = value
576
577 def transform_rest_request(self, orig_request, params, method_parameters):
578 """Translates a Rest request into an apiserving request.
579
580 This makes a copy of orig_request and transforms it to apiserving
581 format (moving request parameters to the body).
582
583 The request can receive values from the path, query and body and combine
584 them before sending them along to the backend. In cases of collision,
585 objects from the body take precedence over those from the query, which in
586 turn take precedence over those from the path.
587
588 In the case that a repeated value occurs in both the query and the path,
589 those values can be combined, but if that value also occurred in the body,
590 it would override any other values.
591
592 In the case of nested values from message fields, non-colliding values
593 from subfields can be combined. For example, if '?a.c=10' occurs in the
594 query string and "{'a': {'b': 11}}" occurs in the body, then they will be
595 combined as
596
597 {
598 'a': {
599 'b': 11,
600 'c': 10,
601 }
602 }
603
604 before being sent to the backend.
605
606 Args:
607 orig_request: An ApiRequest, the original request from the user.
608 params: A dict with URL path parameters extracted by the config_manager
609 lookup.
610 method_parameters: A dictionary containing the API configuration for the
611 parameters for the request.
612
613 Returns:
614 A copy of the current request that's been modified so it can be sent
615 to the backend. The body is updated to include parameters from the
616 URL.
617 """
618 request = orig_request.copy()
619 body_json = {}
620
621 # Handle parameters from the URL path.
622 for key, value in params.iteritems():
623 # Values need to be in a list to interact with query parameter values
624 # and to account for case of repeated parameters
625 body_json[key] = [value]
626
627 # Add in parameters from the query string.
628 if request.parameters:
629 # For repeated elements, query and path work together
630 for key, value in request.parameters.iteritems():
631 if key in body_json:
632 body_json[key] = value + body_json[key]
633 else:
634 body_json[key] = value
635
636 # Validate all parameters we've merged so far and convert any '.' delimited
637 # parameters to nested parameters. We don't use iteritems since we may
638 # modify body_json within the loop. For instance, 'a.b' is not a valid key
639 # and would be replaced with 'a'.
640 for key, value in body_json.items():
641 current_parameter = method_parameters.get(key, {})
642 repeated = current_parameter.get('repeated', False)
643
644 if not repeated:
645 body_json[key] = body_json[key][0]
646
647 # Order is important here. Parameter names are dot-delimited in
648 # parameters instead of nested in dictionaries as a message field is, so
649 # we need to call transform_parameter_value on them before calling
650 # _add_message_field.
651 body_json[key] = parameter_converter.transform_parameter_value(
652 key, body_json[key], current_parameter)
653 # Remove the old key and try to convert to nested message value
654 message_value = body_json.pop(key)
655 self._add_message_field(key, message_value, body_json)
656
657 # Add in values from the body of the request.
658 if request.body_json:
659 self._update_from_body(body_json, request.body_json)
660
661 request.body_json = body_json
662 request.body = json.dumps(request.body_json)
663 return request
664
665 def transform_jsonrpc_request(self, orig_request):
666 """Translates a JsonRpc request/response into apiserving request/response.
667
668 Args:
669 orig_request: An ApiRequest, the original request from the user.
670
671 Returns:
672 A new request with the request_id updated and params moved to the body.
673 """
674 request = orig_request.copy()
675 request.request_id = request.body_json.get('id')
676 request.body_json = request.body_json.get('params', {})
677 request.body = json.dumps(request.body_json)
678 return request
679
680 def check_error_response(self, body, status):
681 """Raise an exception if the response from the backend was an error.
682
683 Args:
684 body: A string containing the backend response body.
685 status: A string containing the backend response status.
686
687 Raises:
688 BackendError if the response is an error.
689 """
690 status_code = int(status.split(' ', 1)[0])
691 if status_code >= 300:
692 raise errors.BackendError(body, status)
693
694 def check_empty_response(self, orig_request, method_config, start_response):
695 """If the response from the backend is empty, return a HTTP 204 No Content.
696
697 Args:
698 orig_request: An ApiRequest, the original request from the user.
699 method_config: A dict, the API config of the method to be called.
700 start_response: A function with semantics defined in PEP-333.
701
702 Returns:
703 If the backend response was empty, this returns a string containing the
704 response body that should be returned to the user. If the backend
705 response wasn't empty, this returns None, indicating that we should not
706 exit early with a 204.
707 """
708 response_config = method_config.get('response', {}).get('body')
709 if response_config == 'empty':
710 # The response to this function should be empty. We should return a 204.
711 # Note that it's possible that the backend returned something, but we'll
712 # ignore it. This matches the behavior in the Endpoints server.
713 cors_handler = self._create_cors_handler(orig_request)
714 return util.send_wsgi_no_content_response(start_response, cors_handler)
715
716 def transform_rest_response(self, response_body):
717 """Translates an apiserving REST response so it's ready to return.
718
719 Currently, the only thing that needs to be fixed here is indentation,
720 so it's consistent with what the live app will return.
721
722 Args:
723 response_body: A string containing the backend response.
724
725 Returns:
726 A reformatted version of the response JSON.
727 """
728 body_json = json.loads(response_body)
729 return json.dumps(body_json, indent=1, sort_keys=True)
730
731 def transform_jsonrpc_response(self, backend_request, response_body):
732 """Translates an apiserving response to a JsonRpc response.
733
734 Args:
735 backend_request: An ApiRequest, the transformed request that was sent to
736 the backend handler.
737 response_body: A string containing the backend response to transform
738 back to JsonRPC.
739
740 Returns:
741 A string with the updated, JsonRPC-formatted request body.
742 """
743 body_json = {'result': json.loads(response_body)}
744 return self._finish_rpc_response(backend_request.request_id,
745 backend_request.is_batch(), body_json)
746
747 def _finish_rpc_response(self, request_id, is_batch, body_json):
748 """Finish adding information to a JSON RPC response.
749
750 Args:
751 request_id: None if the request didn't have a request ID. Otherwise, this
752 is a string containing the request ID for the request.
753 is_batch: A boolean indicating whether the request is a batch request.
754 body_json: A dict containing the JSON body of the response.
755
756 Returns:
757 A string with the updated, JsonRPC-formatted request body.
758 """
759 if request_id is not None:
760 body_json['id'] = request_id
761 if is_batch:
762 body_json = [body_json]
763 return json.dumps(body_json, indent=1, sort_keys=True)
764
765 def _handle_request_error(self, orig_request, error, start_response):
766 """Handle a request error, converting it to a WSGI response.
767
768 Args:
769 orig_request: An ApiRequest, the original request from the user.
770 error: A RequestError containing information about the error.
771 start_response: A function with semantics defined in PEP-333.
772
773 Returns:
774 A string containing the response body.
775 """
776 headers = [('Content-Type', 'application/json')]
777 if orig_request.is_rpc():
778 # JSON RPC errors are returned with status 200 OK and the
779 # error details in the body.
780 status_code = 200
781 body = self._finish_rpc_response(orig_request.body_json.get('id'),
782 orig_request.is_batch(),
783 error.rpc_error())
784 else:
785 status_code = error.status_code()
786 body = error.rest_error()
787
788 response_status = '%d %s' % (status_code,
789 httplib.responses.get(status_code,
790 'Unknown Error'))
791 cors_handler = self._create_cors_handler(orig_request)
792 return util.send_wsgi_response(response_status, headers, body,
793 start_response, cors_handler=cors_handler)
OLDNEW
« no previous file with comments | « third_party/google-endpoints/endpoints/discovery_service.py ('k') | third_party/google-endpoints/endpoints/endpointscfg.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698