rpc-python-toolkit 0.2.0__py3-none-any.whl → 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -57,6 +57,7 @@ class RpcEndpoint:
57
57
  max_batch_size: int = 100,
58
58
  max_serialization_depth: int = 100,
59
59
  max_deserialization_depth: int = 100,
60
+ sanitize_errors: bool = True,
60
61
  ):
61
62
  self.context = context
62
63
  self.methods: dict[str, MethodConfig] = {}
@@ -68,6 +69,7 @@ class RpcEndpoint:
68
69
  self.max_batch_size = max_batch_size
69
70
  self.max_serialization_depth = max_serialization_depth
70
71
  self.max_deserialization_depth = max_deserialization_depth
72
+ self.sanitize_errors = sanitize_errors
71
73
  self.validator = SchemaValidator()
72
74
 
73
75
  if self.enable_introspection:
@@ -117,17 +119,13 @@ class RpcEndpoint:
117
119
  try:
118
120
  request = self._decode_payload(payload)
119
121
  except Exception as exc:
122
+ error = {"code": -32700, "message": "Parse error"}
123
+ if not self.sanitize_errors:
124
+ error["data"] = {"message": str(exc)}
120
125
  return RpcHttpResponse(
121
126
  200,
122
127
  headers,
123
- make_response(
124
- None,
125
- error={
126
- "code": -32700,
127
- "message": "Parse error",
128
- "data": {"message": str(exc)},
129
- },
130
- ),
128
+ make_response(None, error=error),
131
129
  )
132
130
 
133
131
  request_context = context or {}
@@ -211,8 +209,12 @@ class RpcEndpoint:
211
209
  return RpcHttpResponse(200, headers, responses)
212
210
 
213
211
  def _process_single(self, request: Any, context: dict[str, Any]) -> dict[str, Any] | None:
212
+ # An invalid envelope is not a notification, even when it has no id.
213
+ # Do not echo an untrusted id until the complete envelope is validated.
214
+ valid_request = False
214
215
  try:
215
216
  validate_request_envelope(request)
217
+ valid_request = True
216
218
  client_safe_enabled = self._client_safe_enabled(context)
217
219
 
218
220
  if self.safe_enabled and self.strict_mode and not self._has_safe_header(context):
@@ -238,23 +240,24 @@ class RpcEndpoint:
238
240
  result=self.serialize_bigints_and_dates(result),
239
241
  )
240
242
  except RpcError as exc:
241
- if isinstance(request, dict) and not has_id(request):
243
+ if valid_request and not has_id(request):
242
244
  return None
243
- request_id = request.get("id") if isinstance(request, dict) else None
245
+ request_id = request.get("id") if valid_request else None
244
246
  return self._make_error_response(request_id, exc)
245
247
  except Exception as exc:
246
- if isinstance(request, dict) and not has_id(request):
248
+ if valid_request and not has_id(request):
247
249
  return None
248
- request_id = request.get("id") if isinstance(request, dict) else None
250
+ request_id = request.get("id") if valid_request else None
249
251
  return self._make_error_response(request_id, RpcError(str(exc), -32603))
250
252
 
251
253
  def _make_error_response(self, request_id: Any, error: RpcError) -> dict[str, Any]:
254
+ internal_error = self.sanitize_errors and error.code == -32603
252
255
  body = {
253
256
  "code": error.code,
254
- "message": str(error),
257
+ "message": "Internal error" if internal_error else str(error),
255
258
  }
256
259
 
257
- if error.data is not None:
260
+ if not internal_error and error.data is not None:
258
261
  body["data"] = self.serialize_bigints_and_dates(error.data)
259
262
 
260
263
  return make_response(request_id, error=body)
@@ -339,7 +342,10 @@ class RpcEndpoint:
339
342
  payload = payload.decode("utf-8")
340
343
 
341
344
  if isinstance(payload, str):
342
- return json.loads(payload)
345
+ def reject_non_json_constant(value: str) -> None:
346
+ raise ValueError(f"Invalid JSON constant: {value}")
347
+
348
+ return json.loads(payload, parse_constant=reject_non_json_constant)
343
349
 
344
350
  return payload
345
351
 
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import math
3
4
  from typing import Any
4
5
 
5
6
  from .errors import RpcError
@@ -35,8 +36,14 @@ def validate_request_envelope(request: Any) -> None:
35
36
  raise RpcError("Method not specified or invalid", -32600)
36
37
 
37
38
  if "params" in request and not isinstance(request["params"], (dict, list)):
38
- raise RpcError("Invalid params", -32602)
39
-
40
- if "id" in request and not isinstance(request["id"], (str, int, float, type(None))):
41
- raise RpcError("Invalid request id", -32600)
39
+ raise RpcError("Invalid params container", -32600)
40
+
41
+ if "id" in request:
42
+ request_id = request["id"]
43
+ if (
44
+ isinstance(request_id, bool)
45
+ or not isinstance(request_id, (str, int, float, type(None)))
46
+ or (isinstance(request_id, float) and not math.isfinite(request_id))
47
+ ):
48
+ raise RpcError("Invalid request id", -32600)
42
49
 
@@ -1 +1 @@
1
- __version__ = "0.2.0"
1
+ __version__ = "0.2.1"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rpc-python-toolkit
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Framework-agnostic Python JSON-RPC 2.0 toolkit with Safe Mode interoperability.
5
5
  Author: Nicola Carpanese
6
6
  License-Expression: MIT
@@ -44,6 +44,15 @@ Alpha package with the core JSON-RPC behavior implemented.
44
44
  - Safe Mode follows the same `X-RPC-Safe-Enabled` and value marker conventions used by `rpc-express-toolkit`.
45
45
  - The public API may still change before a stable Python release.
46
46
 
47
+ ## Release Notes
48
+
49
+ ### 0.2.1 - 2026-10-12
50
+
51
+ - Sanitize unexpected errors and `RpcError(-32603)` by default, removing internal messages and data while preserving explicit application errors.
52
+ - Validate request envelopes and reject non-JSON numeric constants, scalar/null parameters, boolean IDs, and non-finite IDs.
53
+ - Return errors for invalid batch elements, distinguish explicit null IDs from notifications, and preserve JSON payload shapes.
54
+ - Add error-handling and protocol regression tests, and document the tightened wire contract and trusted handler context.
55
+
47
56
  ## Installation
48
57
 
49
58
  Install from PyPI:
@@ -108,10 +117,26 @@ print(response.body)
108
117
  ```
109
118
 
110
119
  Batch can be disabled with `enable_batch=False`. Introspection remains opt-in.
120
+ Unexpected errors and `RpcError` instances with code `-32603` return only
121
+ `Internal error` by default, without internal messages or data. Other explicit
122
+ `RpcError` codes, messages, and data are preserved. Parse errors omit diagnostic
123
+ data. Use `sanitize_errors=False` only for local debugging to restore details.
124
+
125
+ Only valid requests without an `id` are notifications. An explicit `id: null`
126
+ receives a response; malformed envelopes (including scalar/null `params` or
127
+ boolean/non-finite IDs) return `-32600` with `id: null`. Method parameter schema
128
+ errors remain `-32602`.
129
+ Malformed JSON, including the non-JSON constants `NaN`, `Infinity`, and `-Infinity`,
130
+ returns `-32700`. Invalid elements in a batch produce individual error responses;
131
+ valid notifications remain silent even when their method fails. Objects, arrays,
132
+ and null results retain their JSON shapes.
133
+
111
134
  When both application and request contexts are dictionaries, handlers receive a
112
135
  merged dictionary and request values take precedence. For a non-dictionary
113
136
  application context plus request data, handlers receive `RpcCallContext` with
114
137
  separate `application` and `request` attributes.
138
+ These contexts are supplied by application/adapter code. Client `params` remain a
139
+ separate third handler argument and cannot replace trusted context.
115
140
 
116
141
  Result:
117
142
 
@@ -147,6 +172,10 @@ Safe Mode enables `X-RPC-Safe-Enabled` negotiation and recursive value encoding/
147
172
 
148
173
  Python exposes arbitrary precision integers as `int`. BigInt marker strings from other RPC Toolkit implementations are decoded to Python `int` values when they are real markers, while literal marker-like strings are protected by the `S:` prefix in Safe Mode.
149
174
 
175
+ Safe Mode transforms payload values, not method names, request IDs, error codes,
176
+ or error messages. Application `RpcError.data` is encoded like a result; sanitized
177
+ internal errors expose neither their original message nor their data.
178
+
150
179
  ## Examples
151
180
 
152
181
  Runnable examples are available in [`examples/`](examples/):
@@ -0,0 +1,13 @@
1
+ rpc_python_toolkit/__init__.py,sha256=OR3yfeiE-KEwC6CqAql59PljqW_VzHDyTwCZ5h7iJwU,514
2
+ rpc_python_toolkit/client.py,sha256=QD3vf8OSdDe42522nwWEy2gEyNAjNJzayezvqN6jP5A,5276
3
+ rpc_python_toolkit/endpoint.py,sha256=nucv7Q9gEeiFWQlL8ajdHFcgxjouK2EqqMMP-K_lOs0,12997
4
+ rpc_python_toolkit/errors.py,sha256=en8mmO56IeT8OfRCTheMxNOUN09RpdLqR8X4BQTB6gk,374
5
+ rpc_python_toolkit/protocol.py,sha256=CfI316lATY5qWH0LER5lnqR4c5m7tnh_S5sO4xDPVmc,1360
6
+ rpc_python_toolkit/serialization.py,sha256=gWI0UyxzNj-EATaBkSG9i-b-De-bH49igqBmzkfE9fM,5755
7
+ rpc_python_toolkit/validation.py,sha256=MXibLokBpBq_mDo7yxZ_64S6Mc_S1LWPM6UYsdxTXZg,617
8
+ rpc_python_toolkit/version.py,sha256=HfjVOrpTnmZ-xVFCYSVmX50EXaBQeJteUHG-PD6iQs8,22
9
+ rpc_python_toolkit-0.2.1.dist-info/licenses/LICENSE,sha256=-JOXJbeUglpNN6FaH-5OkUacdU1e8cXsttGJxO8EAAY,1074
10
+ rpc_python_toolkit-0.2.1.dist-info/METADATA,sha256=9h1w-55QH0IpP4Tv2s2ZU1ryriyyqj7rxPZBrlPk5rI,8133
11
+ rpc_python_toolkit-0.2.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
12
+ rpc_python_toolkit-0.2.1.dist-info/top_level.txt,sha256=KeHhMmqjDgfOlP13qPd9_FhW3vXFe2xTL3HzuDAqiv4,19
13
+ rpc_python_toolkit-0.2.1.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- rpc_python_toolkit/__init__.py,sha256=OR3yfeiE-KEwC6CqAql59PljqW_VzHDyTwCZ5h7iJwU,514
2
- rpc_python_toolkit/client.py,sha256=QD3vf8OSdDe42522nwWEy2gEyNAjNJzayezvqN6jP5A,5276
3
- rpc_python_toolkit/endpoint.py,sha256=uTIM2YxiOb6lk4ORMCyg12T64CERFuQWXCjQSd8ditc,12498
4
- rpc_python_toolkit/errors.py,sha256=en8mmO56IeT8OfRCTheMxNOUN09RpdLqR8X4BQTB6gk,374
5
- rpc_python_toolkit/protocol.py,sha256=MMQ1tF1waizZ0dlq3LZc_Dx9tVN00KFLaWh70wR9g7A,1145
6
- rpc_python_toolkit/serialization.py,sha256=gWI0UyxzNj-EATaBkSG9i-b-De-bH49igqBmzkfE9fM,5755
7
- rpc_python_toolkit/validation.py,sha256=MXibLokBpBq_mDo7yxZ_64S6Mc_S1LWPM6UYsdxTXZg,617
8
- rpc_python_toolkit/version.py,sha256=Zn1KFblwuFHiDRdRAiRnDBRkbPttWh44jKa5zG2ov0E,22
9
- rpc_python_toolkit-0.2.0.dist-info/licenses/LICENSE,sha256=-JOXJbeUglpNN6FaH-5OkUacdU1e8cXsttGJxO8EAAY,1074
10
- rpc_python_toolkit-0.2.0.dist-info/METADATA,sha256=eCYHtsYntC9qSy6AxvDv8BLf8-vVlcnbC_mK9MciTiA,6319
11
- rpc_python_toolkit-0.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
12
- rpc_python_toolkit-0.2.0.dist-info/top_level.txt,sha256=KeHhMmqjDgfOlP13qPd9_FhW3vXFe2xTL3HzuDAqiv4,19
13
- rpc_python_toolkit-0.2.0.dist-info/RECORD,,