json_rpc_handler 0.1.1 → 0.2.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4ab4cc44acf0af5fa1885988333068a9a29a92ad7dde26eac57c00316b912daa
4
- data.tar.gz: 3f0f50c63f60a9af530e3be78457f0918a1faadc1b30759e644e788bb12ced9a
3
+ metadata.gz: 4d06db2e48d586ba0b9ccd2597d73d2241aef95f5ff105b12e6888c5042e8d99
4
+ data.tar.gz: 4685164c85a5eeba765f212ad1318eb0de154409b3d79428ac68adbddc33b97d
5
5
  SHA512:
6
- metadata.gz: 2f543d5ca5285ce9668de5a5b6b6475fbe4c38e5cba05468ab7f40a8e24d0a8814ed3e44db47780630e56003c3c6c3347d6bb7b8c3cf80745f93109dff28bf95
7
- data.tar.gz: 7869a59c90ab1a6372817058277e1a1bfcb081bfd7fce2dbb2a9488aab4e5d0dd76c149def372be9d52d6d12dd2513f3e902254f14996a967e7b16b9926297f6
6
+ metadata.gz: e306065b7487a8a77ac999a74966b625a48599ff4b3013bcee2a7e666d6841b0b80b198b18410ade448df63a9eb673c4d30d2401059a5189963b51ccc91fd8c1
7
+ data.tar.gz: c288e6f466591a896bf51b9fa4197b15153e6ccfc3327ae3af304f090eca4e0e91c88f8f21109a57efabde74ba8fc9bd84acb319e4c5ca6a566beeb345c52405
data/README.md CHANGED
@@ -82,6 +82,106 @@ method returned `nil`, or all method calls of a batch request returned `nil`. It
82
82
  is up to the integration to apply the appropriate transport-layer semantics
83
83
  (e.g. returning a 204 No Content).
84
84
 
85
+ ### Error handling
86
+
87
+ Unexpected exceptions from method lookup or a registered method return code
88
+ `-32603` and message `Internal error`, with no `data` member.
89
+
90
+ Pass `on_error:` to `handle` or `handle_json` to report these exceptions. The hook
91
+ receives the exception and a context hash with `id` and `method` keys. It runs
92
+ before the internal error response is built, once for each failing batch item,
93
+ including notifications. The notification's `id` is `nil`.
94
+
95
+ ```ruby
96
+ methods = { 'add' => ->(params) { params[:a] + params[:b] } }
97
+ on_error = ->(exception, context) {
98
+ warn "#{context[:method]} #{context[:id].inspect}: #{exception.message}"
99
+ }
100
+
101
+ JsonRpcHandler.handle(request, on_error:) { |name| methods[name] }
102
+ JsonRpcHandler.handle_json(request_json, on_error:) { |name| methods[name] }
103
+ ```
104
+
105
+ The hook's return value is ignored. If it raises a `StandardError`, that error is
106
+ ignored without calling the hook again. The original request still gets the
107
+ internal error response, or no response for a notification.
108
+
109
+ For an explicit error response, raise `JsonRpcHandler::Error`, a `StandardError`
110
+ subclass. Its constructor takes required `code:` and `message:` keywords and an
111
+ optional `data:` keyword:
112
+
113
+ ```ruby
114
+ response = JsonRpcHandler.handle(request) do |name|
115
+ if name == 'add'
116
+ ->(_params) {
117
+ raise JsonRpcHandler::Error.new(
118
+ code: -32001,
119
+ message: 'Try later',
120
+ data: { retry: true },
121
+ )
122
+ }
123
+ end
124
+ end
125
+
126
+ puts response.to_json
127
+ # {"jsonrpc":"2.0","id":1,"error":{"code":-32001,"message":"Try later","data":{"retry":true}}}
128
+ ```
129
+
130
+ The supplied code, message, and data become the response's error fields. A `nil`
131
+ `data` value is omitted; values such as `false` and `0` are kept. There is no
132
+ fallback from `data` to the exception message. These explicit errors do not call
133
+ `on_error:` and still produce no response for notifications. Other response
134
+ errors, such as Method not found and Parse error, keep their existing fields and
135
+ do not call the hook.
136
+
137
+ ### ID Validation
138
+
139
+ By default, string request IDs are validated to contain only alphanumeric
140
+ characters, dashes, and underscores.
141
+
142
+ **Note:** The JSON-RPC 2.0 specification does not specify a default ID validation pattern, but this default validation
143
+ is recommended to protect against XSS vulnerabilities when IDs are reflected in responses.
144
+
145
+ ```rb
146
+ # Default behavior - accepts alphanumerics, dashes, underscores
147
+ request = { jsonrpc: '2.0', id: 'request-123_abc', method: 'add', params: {a: 1, b: 2} }
148
+ JsonRpcHandler.handle(request) { |method_name| ... }
149
+ # => {"jsonrpc":"2.0","id":"request-123_abc","result":3}
150
+
151
+ # Rejects potentially dangerous characters
152
+ request = { jsonrpc: '2.0', id: '<script>alert("xss")</script>', method: 'add', params: {a: 1, b: 2} }
153
+ JsonRpcHandler.handle(request) { |method_name| ... }
154
+ # => {"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Invalid Request","data":"Request ID must match validation pattern, or be an integer or null"}}
155
+ ```
156
+
157
+ You can customize the validation pattern by passing the `id_validation_pattern`
158
+ parameter:
159
+
160
+ ```rb
161
+ # Allow email-like IDs with a custom pattern
162
+ custom_pattern = /\A[a-zA-Z0-9_.\-@]+\z/
163
+ request = { jsonrpc: '2.0', id: 'user@example.com', method: 'add', params: {a: 1, b: 2} }
164
+
165
+ JsonRpcHandler.handle(request, id_validation_pattern: custom_pattern) do |method_name|
166
+ # ...
167
+ end
168
+
169
+ # Also works with handle_json
170
+ JsonRpcHandler.handle_json(request_json, id_validation_pattern: custom_pattern) do |method_name|
171
+ # ...
172
+ end
173
+ ```
174
+
175
+ To disable ID validation entirely (not recommended), pass
176
+ `nil` as the pattern:
177
+
178
+ ```rb
179
+ # Accepts any string
180
+ JsonRpcHandler.handle(request, id_validation_pattern: nil) do |method_name|
181
+ # ...
182
+ end
183
+ ```
184
+
85
185
  ## Development
86
186
 
87
187
  After checking out the repo:
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JsonRpcHandler
4
- VERSION = '0.1.1'
4
+ VERSION = '0.2.0'
5
5
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json_rpc_handler/version'
4
+ require 'json'
4
5
 
5
6
  module JsonRpcHandler
6
7
  class Version
@@ -16,18 +17,31 @@ module JsonRpcHandler
16
17
  ParseError = -32700
17
18
  end
18
19
 
20
+ class Error < StandardError
21
+ attr_reader :code, :data
22
+
23
+ def initialize(code:, message:, data: nil)
24
+ super(message)
25
+ @code = code
26
+ @data = data
27
+ end
28
+ end
29
+
30
+ DEFAULT_ALLOWED_ID_CHARACTERS = /\A[a-zA-Z0-9_-]+\z/.freeze
31
+
19
32
  module_function
20
33
 
21
- def handle(request, &method_finder)
34
+ def handle(request, id_validation_pattern: DEFAULT_ALLOWED_ID_CHARACTERS, on_error: nil, &method_finder)
35
+
22
36
  if request.is_a? Array
23
- return error_response id: :unknown_id, error: {
37
+ return error_response id: :unknown_id, id_validation_pattern:, error: {
24
38
  code: ErrorCode::InvalidRequest,
25
39
  message: 'Invalid Request',
26
40
  data: 'Request is an empty array',
27
41
  } if request.empty?
28
42
 
29
43
  # Handle batch requests
30
- responses = request.map { |req| process_request req, &method_finder }.compact
44
+ responses = request.map { |req| process_request req, id_validation_pattern:, on_error:, &method_finder }.compact
31
45
 
32
46
  # A single item is hoisted out of the array
33
47
  return responses.first if responses.one?
@@ -36,9 +50,9 @@ module JsonRpcHandler
36
50
  responses if responses.any?
37
51
  elsif request.is_a? Hash
38
52
  # Handle single request
39
- process_request request, &method_finder
53
+ process_request request, id_validation_pattern:, on_error:, &method_finder
40
54
  else
41
- error_response id: :unknown_id, error: {
55
+ error_response id: :unknown_id, id_validation_pattern:, error: {
42
56
  code: ErrorCode::InvalidRequest,
43
57
  message: 'Invalid Request',
44
58
  data: 'Request must be an array or a hash',
@@ -46,12 +60,12 @@ module JsonRpcHandler
46
60
  end
47
61
  end
48
62
 
49
- def handle_json(request_json, &method_finder)
63
+ def handle_json(request_json, id_validation_pattern: DEFAULT_ALLOWED_ID_CHARACTERS, on_error: nil, &method_finder)
50
64
  begin
51
65
  request = JSON.parse request_json, symbolize_names: true
52
- response = handle request, &method_finder
66
+ response = handle request, id_validation_pattern:, on_error:, &method_finder
53
67
  rescue JSON::ParserError
54
- response =error_response id: :unknown_id, error: {
68
+ response = error_response id: :unknown_id, id_validation_pattern:, error: {
55
69
  code: ErrorCode::ParseError,
56
70
  message: 'Parse error',
57
71
  data: 'Invalid JSON',
@@ -61,16 +75,16 @@ module JsonRpcHandler
61
75
  response.to_json if response
62
76
  end
63
77
 
64
- def process_request(request, &method_finder)
78
+ def process_request(request, id_validation_pattern:, on_error: nil, &method_finder)
65
79
  id = request[:id]
66
80
 
67
81
  error = case
68
82
  when !valid_version?(request[:jsonrpc]) then 'JSON-RPC version must be 2.0'
69
- when !valid_id?(request[:id]) then 'Request ID must be a string or an integer or null'
83
+ when !valid_id?(request[:id], id_validation_pattern) then 'Request ID must match validation pattern, or be an integer or null'
70
84
  when !valid_method_name?(request[:method]) then 'Method name must be a string and not start with "rpc."'
71
85
  end
72
86
 
73
- return error_response id: :unknown_id, error: {
87
+ return error_response id: :unknown_id, id_validation_pattern:, error: {
74
88
  code: ErrorCode::InvalidRequest,
75
89
  message: 'Invalid Request',
76
90
  data: error,
@@ -80,7 +94,7 @@ module JsonRpcHandler
80
94
  params = request[:params]
81
95
 
82
96
  unless valid_params? params
83
- return error_response id:, error: {
97
+ return error_response id:, id_validation_pattern:, error: {
84
98
  code: ErrorCode::InvalidParams,
85
99
  message: 'Invalid params',
86
100
  data: 'Method parameters must be an array or an object or null',
@@ -91,7 +105,7 @@ module JsonRpcHandler
91
105
  method = method_finder.call method_name
92
106
 
93
107
  if method.nil?
94
- return error_response id:, error: {
108
+ return error_response id:, id_validation_pattern:, error: {
95
109
  code: ErrorCode::MethodNotFound,
96
110
  message: 'Method not found',
97
111
  data: method_name,
@@ -101,11 +115,22 @@ module JsonRpcHandler
101
115
  result = method.call params
102
116
 
103
117
  success_response id:, result:
118
+ rescue Error => e
119
+ error_response id:, id_validation_pattern:, error: {
120
+ code: e.code,
121
+ message: e.message,
122
+ data: e.data,
123
+ }
104
124
  rescue StandardError => e
105
- error_response id:, error: {
125
+ begin
126
+ on_error&.call(e, { id:, method: method_name })
127
+ rescue StandardError
128
+ # Reporter errors must not change the response.
129
+ end
130
+
131
+ error_response id:, id_validation_pattern:, error: {
106
132
  code: ErrorCode::InternalError,
107
133
  message: 'Internal error',
108
- data: e.message,
109
134
  }
110
135
  end
111
136
  end
@@ -114,8 +139,12 @@ module JsonRpcHandler
114
139
  version == Version::V2_0
115
140
  end
116
141
 
117
- def valid_id?(id)
118
- id.is_a?(String) || id.is_a?(Integer) || id.nil?
142
+
143
+ def valid_id?(id, pattern = nil)
144
+ return true if id.nil? || id.is_a?(Integer)
145
+ return false unless id.is_a?(String)
146
+
147
+ pattern ? id.match?(pattern) : true
119
148
  end
120
149
 
121
150
  def valid_method_name?(method)
@@ -134,10 +163,10 @@ module JsonRpcHandler
134
163
  } unless id.nil?
135
164
  end
136
165
 
137
- def error_response(id:, error:)
166
+ def error_response(id:, id_validation_pattern:, error:)
138
167
  {
139
168
  jsonrpc: Version::V2_0,
140
- id: valid_id?(id) ? id : nil,
169
+ id: valid_id?(id, id_validation_pattern) ? id : nil,
141
170
  error: error.compact,
142
171
  } unless id.nil?
143
172
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: json_rpc_handler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ates Goral
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-03-12 00:00:00.000000000 Z
10
+ date: 2026-11-10 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  email:
13
13
  - ates.goral@shopify.com
@@ -43,7 +43,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
43
43
  - !ruby/object:Gem::Version
44
44
  version: 1.3.7
45
45
  requirements: []
46
- rubygems_version: 3.6.5
46
+ rubygems_version: 3.6.2
47
47
  specification_version: 4
48
48
  summary: A spec-compliant JSON-RPC 2.0 handler
49
49
  test_files: []