##// END OF EJS Templates
api: fixes and changes to always return content type in API...
super-admin -
r5001:961992a2 default
parent child Browse files
Show More
@@ -21,10 +21,10 b''
21 import itertools
21 import itertools
22 import logging
22 import logging
23 import sys
23 import sys
24 import types
25 import fnmatch
24 import fnmatch
26
25
27 import decorator
26 import decorator
27 import typing
28 import venusian
28 import venusian
29 from collections import OrderedDict
29 from collections import OrderedDict
30
30
@@ -39,7 +39,7 b' from rhodecode.apps._base import Templat'
39 from rhodecode.lib.auth import AuthUser
39 from rhodecode.lib.auth import AuthUser
40 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
40 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
41 from rhodecode.lib.exc_tracking import store_exception
41 from rhodecode.lib.exc_tracking import store_exception
42 from rhodecode.lib.ext_json import json
42 from rhodecode.lib import ext_json
43 from rhodecode.lib.utils2 import safe_str
43 from rhodecode.lib.utils2 import safe_str
44 from rhodecode.lib.plugins.utils import get_plugin_settings
44 from rhodecode.lib.plugins.utils import get_plugin_settings
45 from rhodecode.model.db import User, UserApiKeys
45 from rhodecode.model.db import User, UserApiKeys
@@ -64,15 +64,12 b' def find_methods(jsonrpc_methods, patter'
64
64
65 class ExtJsonRenderer(object):
65 class ExtJsonRenderer(object):
66 """
66 """
67 Custom renderer that mkaes use of our ext_json lib
67 Custom renderer that makes use of our ext_json lib
68
68
69 """
69 """
70
70
71 def __init__(self, serializer=json.dumps, **kw):
71 def __init__(self):
72 """ Any keyword arguments will be passed to the ``serializer``
72 self.serializer = ext_json.formatted_json
73 function."""
74 self.serializer = serializer
75 self.kw = kw
76
73
77 def __call__(self, info):
74 def __call__(self, info):
78 """ Returns a plain JSON-encoded string with content-type
75 """ Returns a plain JSON-encoded string with content-type
@@ -87,25 +84,17 b' class ExtJsonRenderer(object):'
87 if ct == response.default_content_type:
84 if ct == response.default_content_type:
88 response.content_type = 'application/json'
85 response.content_type = 'application/json'
89
86
90 return self.serializer(value, **self.kw)
87 return self.serializer(value)
91
88
92 return _render
89 return _render
93
90
94
91
95 def jsonrpc_response(request, result):
92 def jsonrpc_response(request, result):
96 rpc_id = getattr(request, 'rpc_id', None)
93 rpc_id = getattr(request, 'rpc_id', None)
97 response = request.response
98
99 # store content_type before render is called
100 ct = response.content_type
101
94
102 ret_value = ''
95 ret_value = ''
103 if rpc_id:
96 if rpc_id:
104 ret_value = {
97 ret_value = {'id': rpc_id, 'result': result, 'error': None}
105 'id': rpc_id,
106 'result': result,
107 'error': None,
108 }
109
98
110 # fetch deprecation warnings, and store it inside results
99 # fetch deprecation warnings, and store it inside results
111 deprecation = getattr(request, 'rpc_deprecation', None)
100 deprecation = getattr(request, 'rpc_deprecation', None)
@@ -113,30 +102,36 b' def jsonrpc_response(request, result):'
113 ret_value['DEPRECATION_WARNING'] = deprecation
102 ret_value['DEPRECATION_WARNING'] = deprecation
114
103
115 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
104 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
116 response.body = safe_str(raw_body, response.charset)
105 content_type = 'application/json'
117
106 content_type_header = 'Content-Type'
118 if ct == response.default_content_type:
107 headers = {
119 response.content_type = 'application/json'
108 content_type_header: content_type
120
109 }
121 return response
110 return Response(
111 body=raw_body,
112 content_type=content_type,
113 headerlist=[(k, v) for k, v in headers.items()]
114 )
122
115
123
116
124 def jsonrpc_error(request, message, retid=None, code=None, headers=None):
117 def jsonrpc_error(request, message, retid=None, code: typing.Optional[int] = None, headers: typing.Optional[dict] = None):
125 """
118 """
126 Generate a Response object with a JSON-RPC error body
119 Generate a Response object with a JSON-RPC error body
120 """
121 headers = headers or {}
122 content_type = 'application/json'
123 content_type_header = 'Content-Type'
124 if content_type_header not in headers:
125 headers[content_type_header] = content_type
127
126
128 :param code:
129 :param retid:
130 :param message:
131 """
132 err_dict = {'id': retid, 'result': None, 'error': message}
127 err_dict = {'id': retid, 'result': None, 'error': message}
133 body = render(DEFAULT_RENDERER, err_dict, request=request).encode('utf-8')
128 raw_body = render(DEFAULT_RENDERER, err_dict, request=request)
134
129
135 return Response(
130 return Response(
136 body=body,
131 body=raw_body,
137 status=code,
132 status=code,
138 content_type='application/json',
133 content_type=content_type,
139 headerlist=headers
134 headerlist=[(k, v) for k, v in headers.items()]
140 )
135 )
141
136
142
137
@@ -158,11 +153,11 b' def exception_view(exc, request):'
158 method = request.rpc_method
153 method = request.rpc_method
159 log.debug('json-rpc method `%s` not found in list of '
154 log.debug('json-rpc method `%s` not found in list of '
160 'api calls: %s, rpc_id:%s',
155 'api calls: %s, rpc_id:%s',
161 method, request.registry.jsonrpc_methods.keys(), rpc_id)
156 method, list(request.registry.jsonrpc_methods.keys()), rpc_id)
162
157
163 similar = 'none'
158 similar = 'none'
164 try:
159 try:
165 similar_paterns = ['*{}*'.format(x) for x in method.split('_')]
160 similar_paterns = [f'*{x}*' for x in method.split('_')]
166 similar_found = find_methods(
161 similar_found = find_methods(
167 request.registry.jsonrpc_methods, similar_paterns)
162 request.registry.jsonrpc_methods, similar_paterns)
168 similar = ', '.join(similar_found.keys()) or similar
163 similar = ', '.join(similar_found.keys()) or similar
@@ -222,7 +217,7 b' def request_view(request):'
222
217
223 # register our auth-user
218 # register our auth-user
224 request.rpc_user = auth_u
219 request.rpc_user = auth_u
225 request.environ['rc_auth_user_id'] = auth_u.user_id
220 request.environ['rc_auth_user_id'] = str(auth_u.user_id)
226
221
227 # now check if token is valid for API
222 # now check if token is valid for API
228 auth_token = request.rpc_api_key
223 auth_token = request.rpc_api_key
@@ -246,10 +241,12 b' def request_view(request):'
246
241
247 # now that we have a method, add request._req_params to
242 # now that we have a method, add request._req_params to
248 # self.kargs and dispatch control to WGIController
243 # self.kargs and dispatch control to WGIController
244
249 argspec = inspect.getargspec(func)
245 argspec = inspect.getargspec(func)
250 arglist = argspec[0]
246 arglist = argspec[0]
251 defaults = map(type, argspec[3] or [])
247 defs = argspec[3] or []
252 default_empty = types.NotImplementedType
248 defaults = [type(a) for a in defs]
249 default_empty = type(NotImplemented)
253
250
254 # kw arguments required by this method
251 # kw arguments required by this method
255 func_kwargs = dict(itertools.zip_longest(
252 func_kwargs = dict(itertools.zip_longest(
@@ -285,7 +282,7 b' def request_view(request):'
285 )
282 )
286
283
287 # sanitize extra passed arguments
284 # sanitize extra passed arguments
288 for k in request.rpc_params.keys()[:]:
285 for k in list(request.rpc_params.keys()):
289 if k not in func_kwargs:
286 if k not in func_kwargs:
290 del request.rpc_params[k]
287 del request.rpc_params[k]
291
288
@@ -313,8 +310,10 b' def request_view(request):'
313 exc_info = sys.exc_info()
310 exc_info = sys.exc_info()
314 exc_id, exc_type_name = store_exception(
311 exc_id, exc_type_name = store_exception(
315 id(exc_info), exc_info, prefix='rhodecode-api')
312 id(exc_info), exc_info, prefix='rhodecode-api')
316 error_headers = [('RhodeCode-Exception-Id', str(exc_id)),
313 error_headers = {
317 ('RhodeCode-Exception-Type', str(exc_type_name))]
314 'RhodeCode-Exception-Id': str(exc_id),
315 'RhodeCode-Exception-Type': str(exc_type_name)
316 }
318 err_resp = jsonrpc_error(
317 err_resp = jsonrpc_error(
319 request, retid=request.rpc_id, message='Internal server error',
318 request, retid=request.rpc_id, message='Internal server error',
320 headers=error_headers)
319 headers=error_headers)
@@ -355,7 +354,7 b' def setup_request(request):'
355 raw_body = request.body
354 raw_body = request.body
356 log.debug("Loading JSON body now")
355 log.debug("Loading JSON body now")
357 try:
356 try:
358 json_body = json.loads(raw_body)
357 json_body = ext_json.json.loads(raw_body)
359 except ValueError as e:
358 except ValueError as e:
360 # catch JSON errors Here
359 # catch JSON errors Here
361 raise JSONRPCError("JSON parse error ERR:%s RAW:%r" % (e, raw_body))
360 raise JSONRPCError("JSON parse error ERR:%s RAW:%r" % (e, raw_body))
@@ -382,7 +381,7 b' def setup_request(request):'
382
381
383 log.debug('method: %s, params: %.10240r', request.rpc_method, request.rpc_params)
382 log.debug('method: %s, params: %.10240r', request.rpc_method, request.rpc_params)
384 except KeyError as e:
383 except KeyError as e:
385 raise JSONRPCError('Incorrect JSON data. Missing %s' % e)
384 raise JSONRPCError(f'Incorrect JSON data. Missing {e}')
386
385
387 log.debug('setup complete, now handling method:%s rpcid:%s',
386 log.debug('setup complete, now handling method:%s rpcid:%s',
388 request.rpc_method, request.rpc_id, )
387 request.rpc_method, request.rpc_id, )
@@ -561,8 +560,7 b' def includeme(config):'
561 config.add_view_predicate('jsonrpc_method', MethodPredicate)
560 config.add_view_predicate('jsonrpc_method', MethodPredicate)
562 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
561 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
563
562
564 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
563 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer())
565 serializer=json.dumps, indent=4))
566 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
564 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
567
565
568 config.add_route_predicate(
566 config.add_route_predicate(
@@ -63,7 +63,7 b' class TestApi(object):'
63
63
64 def test_api_missing_non_optional_param_args_null(self):
64 def test_api_missing_non_optional_param_args_null(self):
65 id_, params = build_data(self.apikey, 'get_repo')
65 id_, params = build_data(self.apikey, 'get_repo')
66 params = params.replace('"args": {}', '"args": null')
66 params = params.replace(b'"args": {}', b'"args": null')
67 response = api_call(self.app, params)
67 response = api_call(self.app, params)
68
68
69 expected = 'Missing non optional `repoid` arg in JSON DATA'
69 expected = 'Missing non optional `repoid` arg in JSON DATA'
@@ -71,7 +71,7 b' class TestApi(object):'
71
71
72 def test_api_missing_non_optional_param_args_bad(self):
72 def test_api_missing_non_optional_param_args_bad(self):
73 id_, params = build_data(self.apikey, 'get_repo')
73 id_, params = build_data(self.apikey, 'get_repo')
74 params = params.replace('"args": {}', '"args": 1')
74 params = params.replace(b'"args": {}', b'"args": 1')
75 response = api_call(self.app, params)
75 response = api_call(self.app, params)
76
76
77 expected = 'Missing non optional `repoid` arg in JSON DATA'
77 expected = 'Missing non optional `repoid` arg in JSON DATA'
@@ -111,13 +111,13 b' class TestApi(object):'
111
111
112 def test_api_args_is_null(self):
112 def test_api_args_is_null(self):
113 __, params = build_data(self.apikey, 'get_users', )
113 __, params = build_data(self.apikey, 'get_users', )
114 params = params.replace('"args": {}', '"args": null')
114 params = params.replace(b'"args": {}', b'"args": null')
115 response = api_call(self.app, params)
115 response = api_call(self.app, params)
116 assert response.status == '200 OK'
116 assert response.status == '200 OK'
117
117
118 def test_api_args_is_bad(self):
118 def test_api_args_is_bad(self):
119 __, params = build_data(self.apikey, 'get_users', )
119 __, params = build_data(self.apikey, 'get_users', )
120 params = params.replace('"args": {}', '"args": 1')
120 params = params.replace(b'"args": {}', b'"args": 1')
121 response = api_call(self.app, params)
121 response = api_call(self.app, params)
122 assert response.status == '200 OK'
122 assert response.status == '200 OK'
123
123
@@ -86,7 +86,8 b' def build_data(apikey, method, **kw):'
86
86
87 def api_call(app, params, status=None):
87 def api_call(app, params, status=None):
88 response = app.post(
88 response = app.post(
89 API_URL, content_type='application/json', params=params, status=status)
89 API_URL, content_type='application/json', params=params, status=status,
90 headers=[('Content-Type', 'application/json')])
90 return response
91 return response
91
92
92
93
@@ -470,6 +470,8 b' def get_repo_nodes(request, apiuser, rep'
470
470
471 ret_type = Optional.extract(ret_type)
471 ret_type = Optional.extract(ret_type)
472 details = Optional.extract(details)
472 details = Optional.extract(details)
473 max_file_bytes = Optional.extract(max_file_bytes)
474
473 _extended_types = ['basic', 'full']
475 _extended_types = ['basic', 'full']
474 if details not in _extended_types:
476 if details not in _extended_types:
475 raise JSONRPCError('ret_type must be one of %s' % (','.join(_extended_types)))
477 raise JSONRPCError('ret_type must be one of %s' % (','.join(_extended_types)))
@@ -492,6 +494,7 b' def get_repo_nodes(request, apiuser, rep'
492 repo, revision, root_path, flat=False,
494 repo, revision, root_path, flat=False,
493 extended_info=extended_info, content=content,
495 extended_info=extended_info, content=content,
494 max_file_bytes=max_file_bytes)
496 max_file_bytes=max_file_bytes)
497
495 _map = {
498 _map = {
496 'all': _d + _f,
499 'all': _d + _f,
497 'files': _f,
500 'files': _f,
@@ -347,7 +347,7 b' def get_method(request, apiuser, pattern'
347
347
348 argspec = inspect.getargspec(func)
348 argspec = inspect.getargspec(func)
349 arglist = argspec[0]
349 arglist = argspec[0]
350 defaults = map(repr, argspec[3] or [])
350 defaults = list(map(repr, argspec[3] or []))
351
351
352 default_empty = '<RequiredType>'
352 default_empty = '<RequiredType>'
353
353
@@ -39,7 +39,7 b' def test(request, apiuser, args):'
39 @jsonrpc_method()
39 @jsonrpc_method()
40 def test_ok(request, apiuser):
40 def test_ok(request, apiuser):
41 return {
41 return {
42 'who': u'hello {} '.format(apiuser),
42 'who': f'hello {apiuser}',
43 'obj': {
43 'obj': {
44 'time': time.time(),
44 'time': time.time(),
45 'dt': datetime.datetime.now(),
45 'dt': datetime.datetime.now(),
@@ -55,12 +55,15 b' def test_error(request, apiuser):'
55
55
56 @jsonrpc_method()
56 @jsonrpc_method()
57 def test_exception(request, apiuser):
57 def test_exception(request, apiuser):
58 raise Exception('something unhanddled')
58 raise Exception('something unhandled')
59
59
60
60
61 @jsonrpc_method()
61 @jsonrpc_method()
62 def test_params(request, apiuser, params):
62 def test_params(request, apiuser, params):
63 return u'hello apiuser:{} params:{}'.format(apiuser, params)
63 return {
64 'who': f'hello {apiuser}',
65 'params': params
66 }
64
67
65
68
66 @jsonrpc_method()
69 @jsonrpc_method()
@@ -69,16 +72,20 b' def test_params_opt('
69 opt3=Optional(OAttr('apiuser'))):
72 opt3=Optional(OAttr('apiuser'))):
70 opt2 = Optional.extract(opt2)
73 opt2 = Optional.extract(opt2)
71 opt3 = Optional.extract(opt3, evaluate_locals=locals())
74 opt3 = Optional.extract(opt3, evaluate_locals=locals())
72
75 return {
73 return u'hello apiuser:{} params:{}, opt:[{},{},{}]'.format(
76 'who': f'hello {apiuser}',
74 apiuser, params, opt1, opt2, opt3)
77 'params': params,
78 'opts': [
79 opt1, opt2, opt3
80 ]
81 }
75
82
76
83
77 @jsonrpc_method()
84 @jsonrpc_method()
78 @jsonrpc_deprecated_method(
85 @jsonrpc_deprecated_method(
79 use_method='test_ok', deprecated_at_version='4.0.0')
86 use_method='test_ok', deprecated_at_version='4.0.0')
80 def test_deprecated_method(request, apiuser):
87 def test_deprecated_method(request, apiuser):
81 return u'value'
88 return 'value'
82
89
83
90
84 @jsonrpc_method()
91 @jsonrpc_method()
General Comments 0
You need to be logged in to leave comments. Login now