protocol-grpc 0.9.0 → 0.11.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
- checksums.yaml.gz.sig +0 -0
- data/design.md +6 -0
- data/lib/protocol/grpc/body/readable.rb +12 -2
- data/lib/protocol/grpc/body/writable.rb +8 -1
- data/lib/protocol/grpc/header/encoding.rb +63 -0
- data/lib/protocol/grpc/header/message.rb +14 -0
- data/lib/protocol/grpc/header/status.rb +13 -0
- data/lib/protocol/grpc/header/timeout.rb +70 -0
- data/lib/protocol/grpc/header.rb +7 -0
- data/lib/protocol/grpc/metadata.rb +20 -4
- data/lib/protocol/grpc/methods.rb +6 -2
- data/lib/protocol/grpc/middleware.rb +4 -1
- data/lib/protocol/grpc/version.rb +1 -1
- data/readme.md +4 -0
- data/releases.md +4 -0
- data.tar.gz.sig +0 -0
- metadata +3 -1
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 506e98d8cefc5e0357e9e59331d2fd1c8e6ee830b08ccade7650e5e1956dbd83
|
|
4
|
+
data.tar.gz: f1dfa3ff52d82e1c9db41d3536864107cc1bbe58de4a0d4ee4cec0021862b739
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c41ddca4368db85c9d18d9c327147104f2a8c62193b9e4488ff90d662a94bc37bb875bb43e19285b093d5554377bc28fd6c4595e447684a984bbc7cb480d841e
|
|
7
|
+
data.tar.gz: 67935af35d49bc8227280862b82c1823e43fc065487d59fa19759e293d7d6c3569b9955b4c621ae16b202dbb17288fbc2680ff9a7ea0659c28a1f35208bdd82b
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/design.md
CHANGED
|
@@ -266,8 +266,14 @@ module Protocol
|
|
|
266
266
|
# Custom header policy for gRPC
|
|
267
267
|
# Extends Protocol::HTTP::Headers::POLICY with gRPC-specific headers
|
|
268
268
|
HEADER_POLICY = Protocol::HTTP::Headers::POLICY.merge(
|
|
269
|
+
# Request headers:
|
|
270
|
+
"grpc-timeout" => Header::Timeout,
|
|
271
|
+
"grpc-encoding" => Header::Encoding,
|
|
272
|
+
|
|
273
|
+
# Response headers:
|
|
269
274
|
"grpc-status" => Header::Status,
|
|
270
275
|
"grpc-message" => Header::Message,
|
|
276
|
+
|
|
271
277
|
# By default, all other headers follow standard HTTP policy
|
|
272
278
|
# But gRPC allows most metadata to be sent as trailers
|
|
273
279
|
).freeze
|
|
@@ -108,9 +108,19 @@ module Protocol
|
|
|
108
108
|
def decompress(data)
|
|
109
109
|
case @encoding
|
|
110
110
|
when "gzip"
|
|
111
|
-
|
|
111
|
+
# Gzip format: zlib stream with gzip header (RFC 1952)
|
|
112
|
+
# Use MAX_WBITS + 32 to handle gzip header and CRC
|
|
113
|
+
inflater = Zlib::Inflate.new(Zlib::MAX_WBITS + 32)
|
|
114
|
+
result = inflater.inflate(data)
|
|
115
|
+
inflater.close
|
|
116
|
+
result
|
|
112
117
|
when "deflate"
|
|
113
|
-
Zlib
|
|
118
|
+
# Zlib format (RFC 1950) - default window bits handle zlib header
|
|
119
|
+
# This matches HTTP's "deflate" content-encoding
|
|
120
|
+
inflater = Zlib::Inflate.new
|
|
121
|
+
result = inflater.inflate(data)
|
|
122
|
+
inflater.close
|
|
123
|
+
result
|
|
114
124
|
else
|
|
115
125
|
data
|
|
116
126
|
end
|
|
@@ -75,19 +75,26 @@ module Protocol
|
|
|
75
75
|
|
|
76
76
|
protected
|
|
77
77
|
|
|
78
|
-
# Compress data using the
|
|
78
|
+
# Compress data using the specified encoding.
|
|
79
|
+
# Per gRPC spec, compression is per-message and uses standard formats:
|
|
80
|
+
# - gzip: RFC 1952 (gzip format with headers and CRC)
|
|
81
|
+
# - deflate: RFC 1950 (zlib format, not raw deflate, for HTTP compatibility)
|
|
79
82
|
# @parameter data [String] The data to compress
|
|
83
|
+
# @parameter encoding [String | Nil] The encoding to use. If `nil`, uses @encoding.
|
|
80
84
|
# @returns [String] The compressed data
|
|
81
85
|
# @raises [Error] If compression fails
|
|
82
86
|
def compress(data)
|
|
83
87
|
case @encoding
|
|
84
88
|
when "gzip"
|
|
89
|
+
# Use GzipWriter for proper gzip format (includes headers, CRC)
|
|
85
90
|
io = StringIO.new
|
|
86
91
|
gz = Zlib::GzipWriter.new(io, @level)
|
|
87
92
|
gz.write(data)
|
|
88
93
|
gz.close
|
|
89
94
|
io.string
|
|
90
95
|
when "deflate"
|
|
96
|
+
# Use zlib format (RFC 1950) for HTTP compatibility
|
|
97
|
+
# This matches HTTP's "deflate" content-encoding
|
|
91
98
|
Zlib::Deflate.deflate(data, @level)
|
|
92
99
|
else
|
|
93
100
|
data # No compression or identity
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
module Protocol
|
|
7
|
+
module GRPC
|
|
8
|
+
module Header
|
|
9
|
+
# The `grpc-encoding` header represents the message compression encoding.
|
|
10
|
+
#
|
|
11
|
+
# The `grpc-encoding` header specifies the compression algorithm used for the message payload.
|
|
12
|
+
# Common values include "identity" (no compression), "gzip", "deflate", etc.
|
|
13
|
+
# This header can appear in both request and response headers, but not in trailers.
|
|
14
|
+
class Encoding < String
|
|
15
|
+
# Parse an encoding from a header value.
|
|
16
|
+
#
|
|
17
|
+
# @parameter value [String] The header value to parse (e.g., "identity", "gzip").
|
|
18
|
+
# @returns [Encoding] A new Encoding instance.
|
|
19
|
+
def self.parse(value)
|
|
20
|
+
new(value)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Coerce a value to an Encoding instance.
|
|
24
|
+
# Used by Protocol::HTTP::Headers when setting header values.
|
|
25
|
+
#
|
|
26
|
+
# @parameter value [Object] The value to coerce (will be converted to string).
|
|
27
|
+
# @returns [Encoding] A new Encoding instance.
|
|
28
|
+
def self.coerce(value)
|
|
29
|
+
new(value.to_s)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Initialize the encoding header with the given value.
|
|
33
|
+
#
|
|
34
|
+
# @parameter value [String] The encoding value (e.g., "identity", "gzip").
|
|
35
|
+
def initialize(value)
|
|
36
|
+
super(value.to_s)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Check if this encoding represents no compression (identity encoding).
|
|
40
|
+
#
|
|
41
|
+
# @returns [Boolean] `true` if the encoding is "identity" or empty.
|
|
42
|
+
def identity?
|
|
43
|
+
self == "identity" || self.empty?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Merge another encoding value (takes the new value, as encoding should only appear once)
|
|
47
|
+
# @parameter value [String] The new encoding value
|
|
48
|
+
def <<(value)
|
|
49
|
+
replace(value.to_s)
|
|
50
|
+
|
|
51
|
+
return self
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Whether this header is acceptable in HTTP trailers.
|
|
55
|
+
# The `grpc-encoding` header does not appear in trailers.
|
|
56
|
+
# @returns [Boolean] `false`, as grpc-encoding cannot appear in trailers.
|
|
57
|
+
def self.trailer?
|
|
58
|
+
false
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -22,6 +22,20 @@ module Protocol
|
|
|
22
22
|
new(value)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# Coerce a value to a Message instance.
|
|
26
|
+
# Used by Protocol::HTTP::Headers when setting header values.
|
|
27
|
+
# Automatically encodes the message for transmission.
|
|
28
|
+
#
|
|
29
|
+
# @parameter value [Object] The value to coerce (will be encoded if not already a Message).
|
|
30
|
+
# @returns [Message] A new Message instance with encoded content.
|
|
31
|
+
def self.coerce(value)
|
|
32
|
+
if value.is_a?(self)
|
|
33
|
+
value
|
|
34
|
+
else
|
|
35
|
+
new(encode(value.to_s))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
25
39
|
# Initialize the message header with the given value.
|
|
26
40
|
#
|
|
27
41
|
# @parameter value [String] The message value (will be URL-encoded if not already encoded).
|
|
@@ -20,6 +20,19 @@ module Protocol
|
|
|
20
20
|
new(value.to_i)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
# Coerce a value to a Status instance.
|
|
24
|
+
# Used by Protocol::HTTP::Headers when setting header values.
|
|
25
|
+
#
|
|
26
|
+
# @parameter value [Object] The value to coerce (integer or string status code).
|
|
27
|
+
# @returns [Status] A new Status instance.
|
|
28
|
+
def self.coerce(value)
|
|
29
|
+
if value.is_a?(self)
|
|
30
|
+
value
|
|
31
|
+
else
|
|
32
|
+
new(value)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
23
36
|
# Initialize the status header with the given value.
|
|
24
37
|
#
|
|
25
38
|
# @parameter value [String | Integer] The status code as a string or integer.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "../methods"
|
|
7
|
+
|
|
8
|
+
module Protocol
|
|
9
|
+
module GRPC
|
|
10
|
+
module Header
|
|
11
|
+
# The `grpc-timeout` header represents the gRPC request timeout.
|
|
12
|
+
#
|
|
13
|
+
# The `grpc-timeout` header specifies how long the client is willing to wait for an RPC to complete.
|
|
14
|
+
# The format is: value + unit (H=hours, M=minutes, S=seconds, m=milliseconds, u=microseconds, n=nanoseconds).
|
|
15
|
+
# This header appears only in request headers, not in trailers.
|
|
16
|
+
class Timeout < String
|
|
17
|
+
# Parse a timeout from a header value.
|
|
18
|
+
#
|
|
19
|
+
# @parameter value [String] The header value to parse (e.g., "5S", "1000m").
|
|
20
|
+
# @returns [Timeout] A new Timeout instance.
|
|
21
|
+
def self.parse(value)
|
|
22
|
+
new(value)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Coerce a value to a Timeout instance.
|
|
26
|
+
#
|
|
27
|
+
# If a Numeric is provided, it will be formatted as a gRPC timeout string using {Protocol::GRPC::Methods.format_timeout}.
|
|
28
|
+
#
|
|
29
|
+
# @parameter value [String | Numeric] The value to coerce.
|
|
30
|
+
# @returns [Timeout] A new Timeout instance.
|
|
31
|
+
def self.coerce(value)
|
|
32
|
+
if value.is_a?(Numeric)
|
|
33
|
+
return new(Protocol::GRPC::Methods.format_timeout(value))
|
|
34
|
+
else
|
|
35
|
+
return new(value.to_s)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Initialize the timeout header with the given value.
|
|
40
|
+
#
|
|
41
|
+
# @parameter value [String] The timeout value in gRPC format.
|
|
42
|
+
def initialize(value)
|
|
43
|
+
super(value.to_s)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Parse the timeout value to seconds.
|
|
47
|
+
#
|
|
48
|
+
# @returns [Numeric | Nil] Timeout in seconds, or `Nil` if value is invalid.
|
|
49
|
+
def to_seconds
|
|
50
|
+
Protocol::GRPC::Methods.parse_timeout(self)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Merge another timeout value (takes the new value, as timeout should only appear once)
|
|
54
|
+
# @parameter value [String] The new timeout value
|
|
55
|
+
def <<(value)
|
|
56
|
+
replace(value.to_s)
|
|
57
|
+
|
|
58
|
+
return self
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Whether this header is acceptable in HTTP trailers.
|
|
62
|
+
# The `grpc-timeout` header is request-only and does not appear in trailers.
|
|
63
|
+
# @returns [Boolean] `false`, as grpc-timeout cannot appear in trailers.
|
|
64
|
+
def self.trailer?
|
|
65
|
+
false
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
data/lib/protocol/grpc/header.rb
CHANGED
|
@@ -8,6 +8,8 @@ require "protocol/http"
|
|
|
8
8
|
require_relative "status"
|
|
9
9
|
require_relative "header/status"
|
|
10
10
|
require_relative "header/message"
|
|
11
|
+
require_relative "header/timeout"
|
|
12
|
+
require_relative "header/encoding"
|
|
11
13
|
|
|
12
14
|
module Protocol
|
|
13
15
|
module GRPC
|
|
@@ -18,6 +20,11 @@ module Protocol
|
|
|
18
20
|
# Custom header policy for gRPC.
|
|
19
21
|
# Extends Protocol::HTTP::Headers::POLICY with gRPC-specific headers.
|
|
20
22
|
HEADER_POLICY = Protocol::HTTP::Headers::POLICY.merge(
|
|
23
|
+
# Request headers:
|
|
24
|
+
"grpc-timeout" => Header::Timeout,
|
|
25
|
+
"grpc-encoding" => Header::Encoding,
|
|
26
|
+
|
|
27
|
+
# Response headers:
|
|
21
28
|
"grpc-status" => Header::Status,
|
|
22
29
|
"grpc-message" => Header::Message
|
|
23
30
|
# By default, all other headers follow standard HTTP policy, but gRPC allows most metadata to be sent as trailers.
|
|
@@ -75,23 +75,39 @@ module Protocol
|
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
-
#
|
|
78
|
+
# Assign gRPC status, message, and optional backtrace to headers.
|
|
79
|
+
#
|
|
79
80
|
# Whether these become headers or trailers is controlled by the protocol layer.
|
|
81
|
+
#
|
|
80
82
|
# @parameter headers [Protocol::HTTP::Headers]
|
|
81
83
|
# @parameter status [Integer] gRPC status code
|
|
82
84
|
# @parameter message [String | Nil] Optional status message
|
|
83
85
|
# @parameter error [Exception | Nil] Optional error object (used to extract backtrace)
|
|
84
|
-
def self.
|
|
85
|
-
headers["grpc-status"] =
|
|
86
|
-
|
|
86
|
+
def self.assign_status!(headers, status: Status::OK, message: nil, error: nil)
|
|
87
|
+
headers["grpc-status"] = status
|
|
88
|
+
|
|
89
|
+
if error && message.nil?
|
|
90
|
+
# If message is not provided but error is, use error message
|
|
91
|
+
message = error.message
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
if message
|
|
95
|
+
headers["grpc-message"] = message
|
|
96
|
+
end
|
|
87
97
|
|
|
88
98
|
# Add backtrace from error if available
|
|
89
99
|
if error && error.backtrace && !error.backtrace.empty?
|
|
90
100
|
# Assign backtrace array directly - Split header will handle it
|
|
91
101
|
headers["backtrace"] = error.backtrace
|
|
92
102
|
end
|
|
103
|
+
|
|
104
|
+
return headers
|
|
93
105
|
end
|
|
94
106
|
|
|
107
|
+
class << self
|
|
108
|
+
# Backward compatibility alias
|
|
109
|
+
alias add_status! assign_status!
|
|
110
|
+
end
|
|
95
111
|
end
|
|
96
112
|
end
|
|
97
113
|
end
|
|
@@ -32,10 +32,14 @@ module Protocol
|
|
|
32
32
|
# @parameter content_type [String] Content type (default: "application/grpc+proto")
|
|
33
33
|
# @returns [Protocol::HTTP::Headers]
|
|
34
34
|
def self.build_headers(metadata: {}, timeout: nil, content_type: "application/grpc+proto")
|
|
35
|
-
headers = Protocol::HTTP::Headers.new
|
|
35
|
+
headers = Protocol::HTTP::Headers.new(policy: Protocol::GRPC::HEADER_POLICY)
|
|
36
36
|
headers["content-type"] = content_type
|
|
37
37
|
headers["te"] = "trailers"
|
|
38
|
-
|
|
38
|
+
|
|
39
|
+
if timeout
|
|
40
|
+
# Coerced to proper format by header policy:
|
|
41
|
+
headers["grpc-timeout"] = timeout
|
|
42
|
+
end
|
|
39
43
|
|
|
40
44
|
metadata.each do |key, value|
|
|
41
45
|
# Binary headers end with -bin and are base64 encoded:
|
|
@@ -27,6 +27,9 @@ module Protocol
|
|
|
27
27
|
def call(request)
|
|
28
28
|
return super unless grpc_request?(request)
|
|
29
29
|
|
|
30
|
+
# Ensure the request headers are using the gRPC header policy:
|
|
31
|
+
request.headers.policy = Protocol::GRPC::HEADER_POLICY
|
|
32
|
+
|
|
30
33
|
begin
|
|
31
34
|
dispatch(request)
|
|
32
35
|
rescue Error => error
|
|
@@ -64,7 +67,7 @@ module Protocol
|
|
|
64
67
|
headers = Protocol::HTTP::Headers.new([], nil, policy: HEADER_POLICY)
|
|
65
68
|
headers["content-type"] = "application/grpc+proto"
|
|
66
69
|
|
|
67
|
-
Metadata.
|
|
70
|
+
Metadata.assign_status!(headers, status: status_code, message: message, error: error)
|
|
68
71
|
|
|
69
72
|
Protocol::HTTP::Response[200, headers, nil]
|
|
70
73
|
end
|
data/readme.md
CHANGED
|
@@ -28,6 +28,10 @@ Please see the [project documentation](https://socketry.github.io/protocol-grpc/
|
|
|
28
28
|
|
|
29
29
|
Please see the [project releases](https://socketry.github.io/protocol-grpc/releases/index) for all releases.
|
|
30
30
|
|
|
31
|
+
### v0.11.0
|
|
32
|
+
|
|
33
|
+
- Rename `add_status!` to `assign_status!` to better reflect its purpose of assigning status information to headers or trailers.
|
|
34
|
+
|
|
31
35
|
### v0.5.0
|
|
32
36
|
|
|
33
37
|
- Server-side errors now automatically include backtraces in response headers when an error object is provided. Backtraces are transmitted as arrays via Split headers and can be extracted by clients.
|
data/releases.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v0.11.0
|
|
4
|
+
|
|
5
|
+
- Rename `add_status!` to `assign_status!` to better reflect its purpose of assigning status information to headers or trailers.
|
|
6
|
+
|
|
3
7
|
## v0.5.0
|
|
4
8
|
|
|
5
9
|
- Server-side errors now automatically include backtraces in response headers when an error object is provided. Backtraces are transmitted as arrays via Split headers and can be extracted by clients.
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: protocol-grpc
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
@@ -109,8 +109,10 @@ files:
|
|
|
109
109
|
- lib/protocol/grpc/call.rb
|
|
110
110
|
- lib/protocol/grpc/error.rb
|
|
111
111
|
- lib/protocol/grpc/header.rb
|
|
112
|
+
- lib/protocol/grpc/header/encoding.rb
|
|
112
113
|
- lib/protocol/grpc/header/message.rb
|
|
113
114
|
- lib/protocol/grpc/header/status.rb
|
|
115
|
+
- lib/protocol/grpc/header/timeout.rb
|
|
114
116
|
- lib/protocol/grpc/health_check.rb
|
|
115
117
|
- lib/protocol/grpc/interface.rb
|
|
116
118
|
- lib/protocol/grpc/metadata.rb
|
metadata.gz.sig
CHANGED
|
Binary file
|