26
26
27
27
- HTTP connections persist for the life of the AuthServiceProxy object
28
28
(if server supports HTTP/1.1)
29
- - sends protocol 'version' , per JSON-RPC 1.1
29
+ - sends "jsonrpc":"2.0" , per JSON-RPC 2.0
30
30
- sends proper, incrementing 'id'
31
31
- sends Basic HTTP authentication headers
32
32
- parses all JSON numbers that look like floats as Decimal
39
39
import http .client
40
40
import json
41
41
import logging
42
- import os
42
+ import pathlib
43
43
import socket
44
44
import time
45
45
import urllib .parse
@@ -60,9 +60,11 @@ def __init__(self, rpc_error, http_status=None):
60
60
self .http_status = http_status
61
61
62
62
63
- def EncodeDecimal (o ):
63
+ def serialization_fallback (o ):
64
64
if isinstance (o , decimal .Decimal ):
65
65
return str (o )
66
+ if isinstance (o , pathlib .Path ):
67
+ return str (o )
66
68
raise TypeError (repr (o ) + " is not JSON serializable" )
67
69
68
70
class AuthServiceProxy ():
@@ -78,7 +80,10 @@ def __init__(self, service_url, service_name=None, timeout=HTTP_TIMEOUT, connect
78
80
passwd = None if self .__url .password is None else self .__url .password .encode ('utf8' )
79
81
authpair = user + b':' + passwd
80
82
self .__auth_header = b'Basic ' + base64 .b64encode (authpair )
81
- self .timeout = timeout
83
+ # clamp the socket timeout, since larger values can cause an
84
+ # "Invalid argument" exception in Python's HTTP(S) client
85
+ # library on some operating systems (e.g. OpenBSD, FreeBSD)
86
+ self .timeout = min (timeout , 2147483 )
82
87
self ._set_conn (connection )
83
88
84
89
def __getattr__ (self , name ):
@@ -91,73 +96,71 @@ def __getattr__(self, name):
91
96
92
97
def _request (self , method , path , postdata ):
93
98
'''
94
- Do a HTTP request, with retry if we get disconnected (e.g. due to a timeout).
95
- This is a workaround for https://bugs.python.org/issue3566 which is fixed in Python 3.5.
99
+ Do a HTTP request.
96
100
'''
97
101
headers = {'Host' : self .__url .hostname ,
98
102
'User-Agent' : USER_AGENT ,
99
103
'Authorization' : self .__auth_header ,
100
104
'Content-type' : 'application/json' }
101
- if os .name == 'nt' :
102
- # Windows somehow does not like to re-use connections
103
- # TODO: Find out why the connection would disconnect occasionally and make it reusable on Windows
104
- # Avoid "ConnectionAbortedError: [WinError 10053] An established connection was aborted by the software in your host machine"
105
- self ._set_conn ()
106
- try :
107
- self .__conn .request (method , path , postdata , headers )
108
- return self ._get_response ()
109
- except (BrokenPipeError , ConnectionResetError ):
110
- # Python 3.5+ raises BrokenPipeError when the connection was reset
111
- # ConnectionResetError happens on FreeBSD
112
- self .__conn .close ()
113
- self .__conn .request (method , path , postdata , headers )
114
- return self ._get_response ()
115
- except OSError as e :
116
- # Workaround for a bug on macOS. See https://bugs.python.org/issue33450
117
- retry = '[Errno 41] Protocol wrong type for socket' in str (e )
118
- if retry :
119
- self .__conn .close ()
120
- self .__conn .request (method , path , postdata , headers )
121
- return self ._get_response ()
122
- else :
123
- raise
105
+ self .__conn .request (method , path , postdata , headers )
106
+ return self ._get_response ()
107
+
108
+ def _json_dumps (self , obj ):
109
+ return json .dumps (obj , default = serialization_fallback , ensure_ascii = self .ensure_ascii )
124
110
125
111
def get_request (self , * args , ** argsn ):
126
112
AuthServiceProxy .__id_count += 1
127
113
128
- log .debug ("-{}-> {} {}" .format (
114
+ log .debug ("-{}-> {} {} {} " .format (
129
115
AuthServiceProxy .__id_count ,
130
116
self ._service_name ,
131
- json .dumps (args or argsn , default = EncodeDecimal , ensure_ascii = self .ensure_ascii ),
117
+ self ._json_dumps (args ),
118
+ self ._json_dumps (argsn ),
132
119
))
120
+
133
121
if args and argsn :
134
- raise ValueError ('Cannot handle both named and positional arguments' )
135
- return {'version' : '1.1' ,
122
+ params = dict (args = args , ** argsn )
123
+ else :
124
+ params = args or argsn
125
+ return {'jsonrpc' : '2.0' ,
136
126
'method' : self ._service_name ,
137
- 'params' : args or argsn ,
127
+ 'params' : params ,
138
128
'id' : AuthServiceProxy .__id_count }
139
129
140
130
def __call__ (self , * args , ** argsn ):
141
- postdata = json . dumps (self .get_request (* args , ** argsn ), default = EncodeDecimal , ensure_ascii = self . ensure_ascii )
131
+ postdata = self . _json_dumps (self .get_request (* args , ** argsn ))
142
132
response , status = self ._request ('POST' , self .__url .path , postdata .encode ('utf-8' ))
143
- if response ['error' ] is not None :
144
- raise JSONRPCException (response ['error' ], status )
145
- elif 'result' not in response :
146
- raise JSONRPCException ({
147
- 'code' : - 343 , 'message' : 'missing JSON-RPC result' }, status )
148
- elif status != HTTPStatus .OK :
149
- raise JSONRPCException ({
150
- 'code' : - 342 , 'message' : 'non-200 HTTP status code but no JSON-RPC error' }, status )
133
+ # For backwards compatibility tests, accept JSON RPC 1.1 responses
134
+ if 'jsonrpc' not in response :
135
+ if response ['error' ] is not None :
136
+ raise JSONRPCException (response ['error' ], status )
137
+ elif 'result' not in response :
138
+ raise JSONRPCException ({
139
+ 'code' : - 343 , 'message' : 'missing JSON-RPC result' }, status )
140
+ elif status != HTTPStatus .OK :
141
+ raise JSONRPCException ({
142
+ 'code' : - 342 , 'message' : 'non-200 HTTP status code but no JSON-RPC error' }, status )
143
+ else :
144
+ return response ['result' ]
151
145
else :
146
+ assert response ['jsonrpc' ] == '2.0'
147
+ if status != HTTPStatus .OK :
148
+ raise JSONRPCException ({
149
+ 'code' : - 342 , 'message' : 'non-200 HTTP status code' }, status )
150
+ if 'error' in response :
151
+ raise JSONRPCException (response ['error' ], status )
152
+ elif 'result' not in response :
153
+ raise JSONRPCException ({
154
+ 'code' : - 343 , 'message' : 'missing JSON-RPC 2.0 result and error' }, status )
152
155
return response ['result' ]
153
156
154
157
def batch (self , rpc_call_list ):
155
- postdata = json . dumps (list (rpc_call_list ), default = EncodeDecimal , ensure_ascii = self . ensure_ascii )
158
+ postdata = self . _json_dumps (list (rpc_call_list ))
156
159
log .debug ("--> " + postdata )
157
160
response , status = self ._request ('POST' , self .__url .path , postdata .encode ('utf-8' ))
158
161
if status != HTTPStatus .OK :
159
162
raise JSONRPCException ({
160
- 'code' : - 342 , 'message' : 'non-200 HTTP status code but no JSON-RPC error ' }, status )
163
+ 'code' : - 342 , 'message' : 'non-200 HTTP status code' }, status )
161
164
return response
162
165
163
166
def _get_response (self ):
@@ -175,17 +178,31 @@ def _get_response(self):
175
178
raise JSONRPCException ({
176
179
'code' : - 342 , 'message' : 'missing HTTP response from server' })
177
180
181
+ # Check for no-content HTTP status code, which can be returned when an
182
+ # RPC client requests a JSON-RPC 2.0 "notification" with no response.
183
+ # Currently this is only possible if clients call the _request() method
184
+ # directly to send a raw request.
185
+ if http_response .status == HTTPStatus .NO_CONTENT :
186
+ if len (http_response .read ()) != 0 :
187
+ raise JSONRPCException ({'code' : - 342 , 'message' : 'Content received with NO CONTENT status code' })
188
+ return None , http_response .status
189
+
178
190
content_type = http_response .getheader ('Content-Type' )
179
191
if content_type != 'application/json' :
180
192
raise JSONRPCException (
181
193
{'code' : - 342 , 'message' : 'non-JSON HTTP response with \' %i %s\' from server' % (http_response .status , http_response .reason )},
182
194
http_response .status )
183
195
184
- responsedata = http_response .read ().decode ('utf8' )
196
+ data = http_response .read ()
197
+ try :
198
+ responsedata = data .decode ('utf8' )
199
+ except UnicodeDecodeError as e :
200
+ raise JSONRPCException ({
201
+ 'code' : - 342 , 'message' : f'Cannot decode response in utf8 format, content: { data } , exception: { e } ' })
185
202
response = json .loads (responsedata , parse_float = decimal .Decimal )
186
203
elapsed = time .time () - req_start_time
187
204
if "error" in response and response ["error" ] is None :
188
- log .debug ("<-%s- [%.6f] %s" % (response ["id" ], elapsed , json . dumps (response ["result" ], default = EncodeDecimal , ensure_ascii = self . ensure_ascii )))
205
+ log .debug ("<-%s- [%.6f] %s" % (response ["id" ], elapsed , self . _json_dumps (response ["result" ])))
189
206
else :
190
207
log .debug ("<-- [%.6f] %s" % (elapsed , responsedata ))
191
208
return response , http_response .status
0 commit comments