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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7a4e884fa493dfbcc313a9c261b9d0cbeef027dc04b7ad99f031160f9231a8e5
4
- data.tar.gz: f0bb7c75757eef5ae129197304ebe1922b928348b65560512ef110586d49ba66
3
+ metadata.gz: 506e98d8cefc5e0357e9e59331d2fd1c8e6ee830b08ccade7650e5e1956dbd83
4
+ data.tar.gz: f1dfa3ff52d82e1c9db41d3536864107cc1bbe58de4a0d4ee4cec0021862b739
5
5
  SHA512:
6
- metadata.gz: 1a9db6d101f6f41583d0b8c09fd00f7c4aa03de315e6a969484ae9c48b23a102b5c957bd47cd3c99d70c367e944d18f23e36c27436c8d8d897df7339d49bf9f7
7
- data.tar.gz: 90f1f8718e1b147cae4a6140aa2b391cb513abe89fa53708c22f5fa8f40724c36c61f35ad448e5ccbcf5437cb4e9bc4e4ee4b4276ea9f1e26399716d55c6fc5c
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
- Zlib::Gunzip.new.inflate(data)
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::Inflate.inflate(data)
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 configured encoding.
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
@@ -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
- # Add gRPC status, message, and optional backtrace to headers.
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.add_status!(headers, status: Status::OK, message: nil, error: nil)
85
- headers["grpc-status"] = Header::Status.new(status)
86
- headers["grpc-message"] = Header::Message.new(Header::Message.encode(message)) if message
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
- headers["grpc-timeout"] = format_timeout(timeout) if timeout
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.add_status!(headers, status: status_code, message: message, error: error)
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
@@ -7,7 +7,7 @@
7
7
  module Protocol
8
8
  # @namespace
9
9
  module GRPC
10
- VERSION = "0.9.0"
10
+ VERSION = "0.11.0"
11
11
  end
12
12
  end
13
13
 
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.9.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