protocol-grpc 0.15.0 → 0.17.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/lib/protocol/grpc/body/readable.rb +3 -8
- data/lib/protocol/grpc/call.rb +1 -1
- data/lib/protocol/grpc/error.rb +1 -0
- data/lib/protocol/grpc/header/timeout.rb +19 -14
- data/lib/protocol/grpc/metadata.rb +25 -6
- data/lib/protocol/grpc/status.rb +16 -1
- data/lib/protocol/grpc/version.rb +1 -1
- data/lib/protocol/grpc.rb +1 -1
- data/license.md +1 -0
- data/readme.md +15 -14
- data/releases.md +11 -0
- data.tar.gz.sig +0 -0
- metadata +3 -2
- 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: bf0f2ec7ae263ceb0b3191c0f3da1b9a6222dd3e0b5e584cc2ec19fa7ebb8857
|
|
4
|
+
data.tar.gz: 303bf31a141edf34f610e1f2a4432571af30c15b65cfc784d1de00915cecb765
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e8d6063d80afcb48d81acc7ac8b3a568c73b8726bb5e369101a5aae49cd31ab66af5cb483304c578db95cf6d03757b8e92460f389a03216ab5870e2b332e519c
|
|
7
|
+
data.tar.gz: 4b7031d73d14092692c1fa05f0a6960446e50b37dd4088fa0132e89cf4be143f32c74f2ff1554ecf9248de67ed4688520bc98898566cb83dac7f9daef64c74d6
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
|
@@ -48,6 +48,7 @@ module Protocol
|
|
|
48
48
|
|
|
49
49
|
# Read the next gRPC message.
|
|
50
50
|
# Overrides Wrapper#read to transform raw HTTP body chunks into decoded gRPC messages.
|
|
51
|
+
# Errors raised by the underlying body propagate unchanged.
|
|
51
52
|
# @returns [Object | String | Nil] Decoded message, raw binary, or `Nil` if stream ended
|
|
52
53
|
def read
|
|
53
54
|
# Read 5-byte prefix: 1 byte compression flag + 4 bytes length
|
|
@@ -85,14 +86,8 @@ module Protocol
|
|
|
85
86
|
def read_exactly(n)
|
|
86
87
|
# Fill buffer until we have enough data:
|
|
87
88
|
while @buffer.bytesize < n
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
raise Error.new(Status::INTERNAL, "Truncated gRPC frame: expected #{n} bytes, received #{@buffer.bytesize}")
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
# Read chunk from underlying body:
|
|
95
|
-
chunk = @body.read
|
|
89
|
+
# An empty body can still have a pending error, so read to determine EOF:
|
|
90
|
+
chunk = @body&.read
|
|
96
91
|
|
|
97
92
|
if chunk.nil?
|
|
98
93
|
return nil if @buffer.empty?
|
data/lib/protocol/grpc/call.rb
CHANGED
data/lib/protocol/grpc/error.rb
CHANGED
|
@@ -13,24 +13,29 @@ module Protocol
|
|
|
13
13
|
# This header appears only in request headers, not in trailers.
|
|
14
14
|
class Timeout < String
|
|
15
15
|
# The wire format for a gRPC timeout value.
|
|
16
|
-
FORMAT = /\A(?<amount
|
|
16
|
+
FORMAT = /\A(?<amount>\d{1,8})(?<unit>[HMSmun])\z/
|
|
17
17
|
|
|
18
18
|
# Format a timeout duration for the `grpc-timeout` header.
|
|
19
19
|
# @parameter timeout [Numeric] The timeout duration in seconds.
|
|
20
20
|
# @returns [String] The formatted timeout.
|
|
21
21
|
def self.format(timeout)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
22
|
+
raise ArgumentError, "Timeout must be finite and non-negative!" unless timeout.finite? && timeout >= 0
|
|
23
|
+
raise RangeError, "Timeout exceeds the grpc-timeout wire limit!" if timeout > 99_999_999 * 3600
|
|
24
|
+
return "0n" if timeout.zero?
|
|
25
|
+
|
|
26
|
+
nanoseconds = (timeout * 1_000_000_000).ceil
|
|
27
|
+
units = {"H" => 3_600_000_000_000, "M" => 60_000_000_000, "S" => 1_000_000_000, "m" => 1_000_000, "u" => 1000, "n" => 1}
|
|
28
|
+
|
|
29
|
+
# Prefer an exact representation in the largest possible unit:
|
|
30
|
+
units.each do |unit, scale|
|
|
31
|
+
amount, remainder = nanoseconds.divmod(scale)
|
|
32
|
+
return "#{amount}#{unit}" if remainder.zero? && amount <= 99_999_999
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Otherwise round up in the finest unit that fits the wire limit:
|
|
36
|
+
units.reverse_each do |unit, scale|
|
|
37
|
+
amount = (nanoseconds + scale - 1).div(scale)
|
|
38
|
+
return "#{amount}#{unit}" if amount <= 99_999_999
|
|
34
39
|
end
|
|
35
40
|
end
|
|
36
41
|
|
|
@@ -69,7 +74,7 @@ module Protocol
|
|
|
69
74
|
# @raises [ArgumentError] If the timeout value is invalid.
|
|
70
75
|
def to_seconds
|
|
71
76
|
unless match = FORMAT.match(self)
|
|
72
|
-
raise ArgumentError, "Invalid grpc-timeout: #{self.inspect}"
|
|
77
|
+
raise ArgumentError, "Invalid grpc-timeout: #{self.inspect}!"
|
|
73
78
|
end
|
|
74
79
|
|
|
75
80
|
amount = match[:amount].to_i
|
|
@@ -17,7 +17,7 @@ module Protocol
|
|
|
17
17
|
# @parameter timeout [Numeric | Nil] Optional timeout in seconds.
|
|
18
18
|
# @parameter content_type [String] The request content type.
|
|
19
19
|
# @returns [Protocol::HTTP::Headers] The constructed request headers.
|
|
20
|
-
def self.build(metadata: {}, timeout: nil, content_type: "application/grpc
|
|
20
|
+
def self.build(metadata: {}, timeout: nil, content_type: "application/grpc")
|
|
21
21
|
headers = Protocol::HTTP::Headers.new(policy: Protocol::GRPC::HEADER_POLICY)
|
|
22
22
|
headers["content-type"] = content_type
|
|
23
23
|
headers["te"] = "trailers"
|
|
@@ -52,9 +52,9 @@ module Protocol
|
|
|
52
52
|
# Decode binary headers:
|
|
53
53
|
if key.end_with?("-bin")
|
|
54
54
|
if value.is_a?(String)
|
|
55
|
-
value =
|
|
55
|
+
value = decode_binary(value)
|
|
56
56
|
elsif value.is_a?(Array)
|
|
57
|
-
value = value.map{|item|
|
|
57
|
+
value = value.map{|item| decode_binary(item)}
|
|
58
58
|
end
|
|
59
59
|
end
|
|
60
60
|
|
|
@@ -64,6 +64,24 @@ module Protocol
|
|
|
64
64
|
metadata
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
+
# Decode a padded or unpadded binary metadata value.
|
|
68
|
+
# @parameter value [String] The base64 encoded value.
|
|
69
|
+
# @returns [String] The decoded bytes.
|
|
70
|
+
# @raises [ArgumentError] If the value has invalid Base64 characters or padding.
|
|
71
|
+
def self.decode_binary(value)
|
|
72
|
+
# Only supply omitted padding; validate existing padding unchanged:
|
|
73
|
+
unless value.end_with?("=")
|
|
74
|
+
case value.bytesize % 4
|
|
75
|
+
when 2
|
|
76
|
+
value += "=="
|
|
77
|
+
when 3
|
|
78
|
+
value += "="
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
Base64.strict_decode64(value)
|
|
83
|
+
end
|
|
84
|
+
|
|
67
85
|
# Extract gRPC status from headers.
|
|
68
86
|
# Returns Status::UNKNOWN if status is not present.
|
|
69
87
|
#
|
|
@@ -107,8 +125,9 @@ module Protocol
|
|
|
107
125
|
# @parameter headers [Protocol::HTTP::Headers]
|
|
108
126
|
# @parameter status [Integer] gRPC status code
|
|
109
127
|
# @parameter message [String | Nil] Optional status message
|
|
110
|
-
# @parameter error [Exception | Nil] Optional error object
|
|
111
|
-
|
|
128
|
+
# @parameter error [Exception | Nil] Optional error object used for the message.
|
|
129
|
+
# @parameter backtrace [Boolean] Whether to include the error backtrace for debugging.
|
|
130
|
+
def self.assign_status!(headers, status: Status::OK, message: nil, error: nil, backtrace: false)
|
|
112
131
|
headers["grpc-status"] = status
|
|
113
132
|
|
|
114
133
|
if error && message.nil?
|
|
@@ -121,7 +140,7 @@ module Protocol
|
|
|
121
140
|
end
|
|
122
141
|
|
|
123
142
|
# Add backtrace from error if available
|
|
124
|
-
if error && error.backtrace && !error.backtrace.empty?
|
|
143
|
+
if backtrace && error && error.backtrace && !error.backtrace.empty?
|
|
125
144
|
# Assign backtrace array directly - Split header will handle it
|
|
126
145
|
headers["backtrace"] = error.backtrace
|
|
127
146
|
end
|
data/lib/protocol/grpc/status.rb
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2025, by Samuel Williams.
|
|
4
|
+
# Copyright, 2025-2026, by Samuel Williams.
|
|
5
|
+
# Copyright, 2026, by Alex Watt.
|
|
5
6
|
|
|
6
7
|
module Protocol
|
|
7
8
|
module GRPC
|
|
8
9
|
# Provides gRPC status codes and their names.
|
|
9
10
|
module Status
|
|
11
|
+
# Map an HTTP response status when the server did not provide grpc-status.
|
|
12
|
+
# @parameter status [Integer] The HTTP status code.
|
|
13
|
+
# @returns [Integer] The fallback gRPC status code.
|
|
14
|
+
def self.for_http_status(status)
|
|
15
|
+
case status
|
|
16
|
+
when 400 then INTERNAL
|
|
17
|
+
when 401 then UNAUTHENTICATED
|
|
18
|
+
when 403 then PERMISSION_DENIED
|
|
19
|
+
when 404 then UNIMPLEMENTED
|
|
20
|
+
when 429, 502, 503, 504 then UNAVAILABLE
|
|
21
|
+
else UNKNOWN
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
10
25
|
OK = 0
|
|
11
26
|
CANCELLED = 1
|
|
12
27
|
UNKNOWN = 2
|
data/lib/protocol/grpc.rb
CHANGED
data/license.md
CHANGED
data/readme.md
CHANGED
|
@@ -28,6 +28,17 @@ 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.17.0
|
|
32
|
+
|
|
33
|
+
- Preserve underlying body read errors, including failures before a message, during a partial frame, or while finishing the stream. These failures are no longer hidden as EOF or replaced with a truncated-frame error.
|
|
34
|
+
|
|
35
|
+
### v0.16.0
|
|
36
|
+
|
|
37
|
+
- Preserve timeout precision within the eight-digit wire limit, rounding up when necessary.
|
|
38
|
+
- Accept padded and unpadded binary metadata.
|
|
39
|
+
- **Breaking**: Error backtraces are no longer sent to clients by default. Pass `backtrace: true` to `Metadata.assign_status!` to explicitly enable them for debugging.
|
|
40
|
+
- Default request metadata to `application/grpc` and provide `Status.for_http_status` for responses without `grpc-status`.
|
|
41
|
+
|
|
31
42
|
### v0.15.0
|
|
32
43
|
|
|
33
44
|
- **Breaking**: Removed `Protocol::GRPC::Status::DESCRIPTIONS`. Use `Protocol::GRPC::Status::NAMES` for canonical gRPC status names.
|
|
@@ -65,16 +76,6 @@ Please see the [project releases](https://socketry.github.io/protocol-grpc/relea
|
|
|
65
76
|
|
|
66
77
|
- Add `RPC#name`.
|
|
67
78
|
|
|
68
|
-
### v0.3.0
|
|
69
|
-
|
|
70
|
-
- **Breaking**: `Protocol::GRPC::Call` now takes a `response` object parameter instead of separate `response_headers`.
|
|
71
|
-
- **Breaking**: Removed `Call#response_headers` method. Use `call.response.headers` directly.
|
|
72
|
-
- Added `RPC#streaming?` method to check if an RPC is streaming.
|
|
73
|
-
|
|
74
|
-
### v0.2.0
|
|
75
|
-
|
|
76
|
-
- `RPC#method` is always defined (snake case).
|
|
77
|
-
|
|
78
79
|
## See Also
|
|
79
80
|
|
|
80
81
|
- [async-grpc](https://github.com/socketry/async-grpc) — Asynchronous gRPC client and server implementation using this interface.
|
|
@@ -95,16 +96,16 @@ We welcome contributions to this project.
|
|
|
95
96
|
|
|
96
97
|
To run the test suite:
|
|
97
98
|
|
|
98
|
-
```
|
|
99
|
-
bundle exec sus
|
|
99
|
+
``` bash
|
|
100
|
+
$ bundle exec sus
|
|
100
101
|
```
|
|
101
102
|
|
|
102
103
|
### Making Releases
|
|
103
104
|
|
|
104
105
|
To make a new release:
|
|
105
106
|
|
|
106
|
-
```
|
|
107
|
-
bundle exec bake gem:release:patch # or minor or major
|
|
107
|
+
``` bash
|
|
108
|
+
$ bundle exec bake gem:release:patch # or minor or major
|
|
108
109
|
```
|
|
109
110
|
|
|
110
111
|
### Developer Certificate of Origin
|
data/releases.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v0.17.0
|
|
4
|
+
|
|
5
|
+
- Preserve underlying body read errors, including failures before a message, during a partial frame, or while finishing the stream. These failures are no longer hidden as EOF or replaced with a truncated-frame error.
|
|
6
|
+
|
|
7
|
+
## v0.16.0
|
|
8
|
+
|
|
9
|
+
- Preserve timeout precision within the eight-digit wire limit, rounding up when necessary.
|
|
10
|
+
- Accept padded and unpadded binary metadata.
|
|
11
|
+
- **Breaking**: Error backtraces are no longer sent to clients by default. Pass `backtrace: true` to `Metadata.assign_status!` to explicitly enable them for debugging.
|
|
12
|
+
- Default request metadata to `application/grpc` and provide `Status.for_http_status` for responses without `grpc-status`.
|
|
13
|
+
|
|
3
14
|
## v0.15.0
|
|
4
15
|
|
|
5
16
|
- **Breaking**: Removed `Protocol::GRPC::Status::DESCRIPTIONS`. Use `Protocol::GRPC::Status::NAMES` for canonical gRPC status names.
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: protocol-grpc
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.17.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
8
|
+
- Alex Watt
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain:
|
|
10
11
|
- |
|
|
@@ -145,7 +146,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
145
146
|
- !ruby/object:Gem::Version
|
|
146
147
|
version: '0'
|
|
147
148
|
requirements: []
|
|
148
|
-
rubygems_version: 4.0.
|
|
149
|
+
rubygems_version: 4.0.16
|
|
149
150
|
specification_version: 4
|
|
150
151
|
summary: Protocol abstractions for gRPC, built on top of protocol-http.
|
|
151
152
|
test_files: []
|
metadata.gz.sig
CHANGED
|
Binary file
|