json_rpc_handler 0.1.0 → 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 +4 -4
- data/README.md +103 -0
- data/lib/json_rpc_handler/version.rb +1 -1
- data/lib/json_rpc_handler.rb +48 -19
- metadata +7 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4d06db2e48d586ba0b9ccd2597d73d2241aef95f5ff105b12e6888c5042e8d99
|
|
4
|
+
data.tar.gz: 4685164c85a5eeba765f212ad1318eb0de154409b3d79428ac68adbddc33b97d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e306065b7487a8a77ac999a74966b625a48599ff4b3013bcee2a7e666d6841b0b80b198b18410ade448df63a9eb673c4d30d2401059a5189963b51ccc91fd8c1
|
|
7
|
+
data.tar.gz: c288e6f466591a896bf51b9fa4197b15153e6ccfc3327ae3af304f090eca4e0e91c88f8f21109a57efabde74ba8fc9bd84acb319e4c5ca6a566beeb345c52405
|
data/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# json_rpc_handler
|
|
2
2
|
|
|
3
|
+
[](https://rubygems.org/gems/json_rpc_handler)
|
|
4
|
+
[](https://github.com/Shopify/json-rpc-handler/actions/workflows/ci.yml)
|
|
5
|
+
|
|
3
6
|
A lightweight, fully spec-compliant [JSON-RPC 2.0][1] handler.
|
|
4
7
|
|
|
5
8
|
It only deals with the parsing and handling of JSON-RPC requests, producing
|
|
@@ -79,6 +82,106 @@ method returned `nil`, or all method calls of a batch request returned `nil`. It
|
|
|
79
82
|
is up to the integration to apply the appropriate transport-layer semantics
|
|
80
83
|
(e.g. returning a 204 No Content).
|
|
81
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
|
+
|
|
82
185
|
## Development
|
|
83
186
|
|
|
84
187
|
After checking out the repo:
|
data/lib/json_rpc_handler.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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.
|
|
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:
|
|
10
|
+
date: 2026-11-10 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
12
|
email:
|
|
13
13
|
- ates.goral@shopify.com
|
|
@@ -20,13 +20,13 @@ files:
|
|
|
20
20
|
- README.md
|
|
21
21
|
- lib/json_rpc_handler.rb
|
|
22
22
|
- lib/json_rpc_handler/version.rb
|
|
23
|
-
homepage: https://github.com/Shopify/
|
|
23
|
+
homepage: https://github.com/Shopify/json-rpc-handler
|
|
24
24
|
licenses:
|
|
25
25
|
- MIT
|
|
26
26
|
metadata:
|
|
27
|
-
bug_tracker_uri: https://github.com/Shopify/
|
|
28
|
-
changelog_uri: https://github.com/Shopify/
|
|
29
|
-
source_code_uri: https://github.com/Shopify/
|
|
27
|
+
bug_tracker_uri: https://github.com/Shopify/json-rpc-handler/issues
|
|
28
|
+
changelog_uri: https://github.com/Shopify/json-rpc-handler/blob/main/CHANGELOG.md
|
|
29
|
+
source_code_uri: https://github.com/Shopify/json-rpc-handler
|
|
30
30
|
allowed_push_host: https://rubygems.org
|
|
31
31
|
rubygems_mfa_required: 'true'
|
|
32
32
|
rdoc_options: []
|
|
@@ -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.
|
|
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: []
|