2424# trace stored on a previously executed transaction's receipt (see _read_only_envelope).
2525_TX_METHODS : Final = ('sendTransaction' ,)
2626
27+ # Methods whose spec accepts an optional `xdrFormat` param (protocols/rpc:
28+ # GetTransactionRequest.Format, SendTransactionRequest.Format). komet-node supports only
29+ # the default 'base64' format; see _check_xdr_format.
30+ _XDR_FORMAT_METHODS : Final = ('getTransaction' , 'sendTransaction' )
31+
2732_log = logging .getLogger ('komet_node' )
2833
2934
@@ -146,33 +151,66 @@ def shutdown(self) -> None:
146151 # ------------------------------------------------------------------
147152
148153 def _handle (self , body : bytes ) -> bytes :
149- """Parse a raw JSON-RPC body and return the response bytes (the HTTP entry point)."""
154+ """Parse a raw JSON-RPC body and return the response bytes (the HTTP entry point).
155+
156+ An array body is a JSON-RPC 2.0 batch; anything else is a single call. The result
157+ may be empty (no response body) when every request was a notification.
158+ """
150159 try :
151160 req = json .loads (body .decode ('utf-8' ))
152161 except (json .JSONDecodeError , UnicodeDecodeError ):
153162 return _error_bytes (None , - 32700 , 'Parse error' )
154- if not isinstance (req , dict ):
163+ if isinstance (req , list ):
164+ return self ._handle_batch (req )
165+ response = self ._handle_single (req )
166+ return b'' if response is None else response .encode ('utf-8' )
167+
168+ def _handle_batch (self , batch : list [Any ]) -> bytes :
169+ """Answer a JSON-RPC 2.0 batch call: one response per element, in an array.
170+
171+ Per section 6 of the spec: an empty array is itself a single Invalid Request error;
172+ invalid elements each get their own error response; notifications get no response
173+ entry, and a batch of only notifications gets no response body at all. Elements run
174+ sequentially (the server is single-threaded by design, see serve()).
175+ """
176+ if not batch :
155177 return _error_bytes (None , - 32600 , 'Invalid Request' )
178+ responses = [response for element in batch if (response := self ._handle_single (element )) is not None ]
179+ if not responses :
180+ return b''
181+ return ('[' + ',' .join (responses ) + ']' ).encode ('utf-8' )
182+
183+ def _handle_single (self , req : Any ) -> str | None :
184+ """Validate one JSON-RPC request frame and dispatch it.
185+
186+ Returns the response as a JSON string, or ``None`` for a notification — a valid
187+ request frame without an ``id`` member, which per JSON-RPC 2.0 is executed but
188+ never answered, not even with an error.
189+ """
190+ if not isinstance (req , dict ):
191+ return _error_str (None , - 32600 , 'Invalid Request' )
156192 request_id = req .get ('id' )
157193
158194 # Validate the JSON-RPC frame before dispatch (JSON-RPC 2.0):
159195 # - wrong/missing protocol version or a non-string method => Invalid Request
160196 # - params, if present, must be a structured (object) value => else Invalid params
161197 if req .get ('jsonrpc' ) != '2.0' or not isinstance (req .get ('method' ), str ):
162- return _error_bytes (request_id , - 32600 , 'Invalid Request' )
198+ return _error_str (request_id , - 32600 , 'Invalid Request' )
163199 params = req .get ('params' )
164200 if params is None :
165201 params = {}
166- elif not isinstance (params , dict ):
167- return _error_bytes (request_id , - 32602 , 'Invalid params' )
168202
169- try :
170- return self .handle_rpc (req ['method' ], params , request_id ).encode ('utf-8' )
171- except Exception :
172- # An unexpected error must never take down the server thread, but it must not
173- # vanish silently either — log the traceback before returning Internal error.
174- traceback .print_exc ()
175- return _error_bytes (request_id , - 32603 , 'Internal error' )
203+ if not isinstance (params , dict ):
204+ response = _error_str (request_id , - 32602 , 'Invalid params' )
205+ else :
206+ try :
207+ response = self .handle_rpc (req ['method' ], params , request_id )
208+ except Exception :
209+ # An unexpected error must never take down the server thread, but it must
210+ # not vanish silently either — log the traceback, return Internal error.
211+ traceback .print_exc ()
212+ response = _error_str (request_id , - 32603 , 'Internal error' )
213+ return None if 'id' not in req else response
176214
177215 def handle_rpc (self , method : str | None , params : dict [str , Any ], request_id : Any = None ) -> str :
178216 """Dispatch a single JSON-RPC call and return the response envelope as a JSON string.
@@ -183,6 +221,13 @@ def handle_rpc(self, method: str | None, params: dict[str, Any], request_id: Any
183221 _log .info ('request: %s (id=%r)' , method , request_id )
184222 self ._archive_request (method , params , request_id )
185223
224+ # Reject an unsupported xdrFormat up front, before the request does anything —
225+ # in particular before a sendTransaction executes and commits state.
226+ if method in _XDR_FORMAT_METHODS :
227+ format_error = _check_xdr_format (params , request_id )
228+ if format_error is not None :
229+ return format_error
230+
186231 if method in _TX_METHODS :
187232 transaction = params .get ('transaction' )
188233 if not isinstance (transaction , str ):
@@ -305,6 +350,22 @@ def _configure_logging() -> None:
305350 _log .setLevel (logging .INFO )
306351
307352
353+ def _check_xdr_format (params : dict [str , Any ], request_id : Any ) -> str | None :
354+ """Validate the optional ``xdrFormat`` param; ``None`` means the request may proceed.
355+
356+ Only ``'base64'`` (the spec default) is supported. The spec's alternative ``'json'``
357+ format is not implemented in komet-node, so it gets a dedicated error message; any
358+ other value is rejected as invalid. Both cases are Invalid params (-32602), matching
359+ real stellar-rpc's handling of a bad format value.
360+ """
361+ xdr_format = params .get ('xdrFormat' , 'base64' )
362+ if xdr_format == 'base64' :
363+ return None
364+ if xdr_format == 'json' :
365+ return _error_str (request_id , - 32602 , "Invalid params: xdrFormat 'json' is not supported, use 'base64'" )
366+ return _error_str (request_id , - 32602 , "Invalid params: unknown xdrFormat, expected 'base64'" )
367+
368+
308369def _error_str (rpc_id : Any , code : int , message : str ) -> str :
309370 return json .dumps ({'jsonrpc' : '2.0' , 'id' : rpc_id , 'error' : {'code' : code , 'message' : message }})
310371
0 commit comments