protocol-grpc 0.14.0 → 0.16.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: c7aa96e69acd0574147e4c3e8c16fba62e8278d01e094267d3cb3427221d8fa0
4
- data.tar.gz: 303976d191a6b5ff5e20db644ef2f29a71c5b7c346558eac2a3e5122c1197bff
3
+ metadata.gz: f2d0b8c7ab88b83ceb64b1cbcd59828bf95416c35b9eafb94892b6c847b4820c
4
+ data.tar.gz: e9283b64b23825657eb739beddb581bd6e3596264e627e7d0cbe5441cd1220de
5
5
  SHA512:
6
- metadata.gz: 1bc1343e7e2e8e21206139992f9483a1c970b21456f90bfb124d4fdd8728eedb51b921a7d93999ece219fd4b079cb9c9e0b2433750e6ee249c234124a1dfaba3
7
- data.tar.gz: cea3a080a0cea063968a40939451b2c2f044f1e4b9068b64f231272e7f2c2d953f6f7ceabef437762700cf2d36b04a90ffb94dfd43ef76f2e5b4ed55d6e4ba29
6
+ metadata.gz: 67dfce9bbf3d3cf7255579c5fd0467b879a6c23c07bbecb3556ee7154922aeeb834fcca6ba9f7a5e8fa025c40d8db141a53db78cf8293fa5c932c55ee0dd6d09
7
+ data.tar.gz: 7c90ef5c07d6a142e9bcdd1f5938b9253f6e73902b5ab284fb1c1b3f9d4167540ed09a81c900eff8996146ffe400a43cb03bcf9fda76b92c3c89cf383cffa3a4
checksums.yaml.gz.sig CHANGED
Binary file
@@ -19,7 +19,7 @@ $ bundle add protocol-grpc
19
19
  - A {ruby Protocol::GRPC::Body::Writable} class which handles writing gRPC messages to HTTP request/response bodies with automatic framing and encoding.
20
20
  - A {ruby Protocol::GRPC::Middleware} abstract base class for building gRPC server applications.
21
21
  - A {ruby Protocol::GRPC::Call} class which represents the request, response, metadata, and deadline for a single gRPC RPC call.
22
- - A {ruby Protocol::GRPC::Status} module with gRPC status code constants.
22
+ - A {ruby Protocol::GRPC::Status} module with gRPC status code constants and names.
23
23
  - A {ruby Protocol::GRPC::Error} hierarchy for gRPC-specific error handling.
24
24
 
25
25
  ## Integration
@@ -103,6 +103,9 @@ if status != Protocol::GRPC::Status::OK
103
103
  message = Protocol::GRPC::Metadata.extract_message(response.headers)
104
104
  raise Protocol::GRPC::Error.for(status, message)
105
105
  end
106
+
107
+ # Use canonical gRPC status names for logging, metrics, or serialization:
108
+ Protocol::GRPC::Status::NAMES[status] # => "OK"
106
109
  ```
107
110
 
108
111
  ### Server Middleware
data/design.md CHANGED
@@ -496,25 +496,25 @@ module Protocol
496
496
  DATA_LOSS = 15
497
497
  UNAUTHENTICATED = 16
498
498
 
499
- # Status code descriptions
500
- DESCRIPTIONS = {
499
+ # Status code names
500
+ NAMES = {
501
501
  OK => "OK",
502
- CANCELLED => "Cancelled",
503
- UNKNOWN => "Unknown",
504
- INVALID_ARGUMENT => "Invalid Argument",
505
- DEADLINE_EXCEEDED => "Deadline Exceeded",
506
- NOT_FOUND => "Not Found",
507
- ALREADY_EXISTS => "Already Exists",
508
- PERMISSION_DENIED => "Permission Denied",
509
- RESOURCE_EXHAUSTED => "Resource Exhausted",
510
- FAILED_PRECONDITION => "Failed Precondition",
511
- ABORTED => "Aborted",
512
- OUT_OF_RANGE => "Out of Range",
513
- UNIMPLEMENTED => "Unimplemented",
514
- INTERNAL => "Internal",
515
- UNAVAILABLE => "Unavailable",
516
- DATA_LOSS => "Data Loss",
517
- UNAUTHENTICATED => "Unauthenticated"
502
+ CANCELLED => "CANCELLED",
503
+ UNKNOWN => "UNKNOWN",
504
+ INVALID_ARGUMENT => "INVALID_ARGUMENT",
505
+ DEADLINE_EXCEEDED => "DEADLINE_EXCEEDED",
506
+ NOT_FOUND => "NOT_FOUND",
507
+ ALREADY_EXISTS => "ALREADY_EXISTS",
508
+ PERMISSION_DENIED => "PERMISSION_DENIED",
509
+ RESOURCE_EXHAUSTED => "RESOURCE_EXHAUSTED",
510
+ FAILED_PRECONDITION => "FAILED_PRECONDITION",
511
+ ABORTED => "ABORTED",
512
+ OUT_OF_RANGE => "OUT_OF_RANGE",
513
+ UNIMPLEMENTED => "UNIMPLEMENTED",
514
+ INTERNAL => "INTERNAL",
515
+ UNAVAILABLE => "UNAVAILABLE",
516
+ DATA_LOSS => "DATA_LOSS",
517
+ UNAUTHENTICATED => "UNAUTHENTICATED"
518
518
  }.freeze
519
519
  end
520
520
  end
@@ -585,7 +585,7 @@ module Protocol
585
585
  @status_code = status_code
586
586
  @details = details
587
587
  @metadata = metadata
588
- super(message || Status::DESCRIPTIONS[status_code])
588
+ super(message || Status::NAMES[status_code])
589
589
  end
590
590
  end
591
591
 
@@ -17,7 +17,7 @@ module Protocol
17
17
  @status_code = status_code
18
18
  @details = details
19
19
  @metadata = metadata
20
- super(message || Status::DESCRIPTIONS[status_code])
20
+ super(message || Status::NAMES[status_code])
21
21
  end
22
22
 
23
23
  # Map status code to error class
@@ -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>[1-9]\d{0,7})(?<unit>[HMSmun])\z/
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
- if timeout >= 3600
23
- "#{(timeout / 3600).to_i}H"
24
- elsif timeout >= 60
25
- "#{(timeout / 60).to_i}M"
26
- elsif timeout >= 1
27
- "#{timeout.to_i}S"
28
- elsif timeout >= 0.001
29
- "#{(timeout * 1000).to_i}m"
30
- elsif timeout >= 0.000001
31
- "#{(timeout * 1_000_000).to_i}u"
32
- else
33
- "#{(timeout * 1_000_000_000).to_i}n"
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+proto")
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 = Base64.strict_decode64(value)
55
+ value = decode_binary(value)
56
56
  elsif value.is_a?(Array)
57
- value = value.map{|item| Base64.strict_decode64(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 (used to extract backtrace)
111
- def self.assign_status!(headers, status: Status::OK, message: nil, error: nil)
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
@@ -5,8 +5,22 @@
5
5
 
6
6
  module Protocol
7
7
  module GRPC
8
- # Provides gRPC status codes and their descriptions.
8
+ # Provides gRPC status codes and their names.
9
9
  module Status
10
+ # Map an HTTP response status when the server did not provide grpc-status.
11
+ # @parameter status [Integer] The HTTP status code.
12
+ # @returns [Integer] The fallback gRPC status code.
13
+ def self.for_http_status(status)
14
+ case status
15
+ when 400 then INTERNAL
16
+ when 401 then UNAUTHENTICATED
17
+ when 403 then PERMISSION_DENIED
18
+ when 404 then UNIMPLEMENTED
19
+ when 429, 502, 503, 504 then UNAVAILABLE
20
+ else UNKNOWN
21
+ end
22
+ end
23
+
10
24
  OK = 0
11
25
  CANCELLED = 1
12
26
  UNKNOWN = 2
@@ -25,25 +39,25 @@ module Protocol
25
39
  DATA_LOSS = 15
26
40
  UNAUTHENTICATED = 16
27
41
 
28
- # Status code descriptions
29
- DESCRIPTIONS = {
42
+ # Status code names, as defined by the gRPC specification.
43
+ NAMES = {
30
44
  OK => "OK",
31
- CANCELLED => "Cancelled",
32
- UNKNOWN => "Unknown",
33
- INVALID_ARGUMENT => "Invalid Argument",
34
- DEADLINE_EXCEEDED => "Deadline Exceeded",
35
- NOT_FOUND => "Not Found",
36
- ALREADY_EXISTS => "Already Exists",
37
- PERMISSION_DENIED => "Permission Denied",
38
- RESOURCE_EXHAUSTED => "Resource Exhausted",
39
- FAILED_PRECONDITION => "Failed Precondition",
40
- ABORTED => "Aborted",
41
- OUT_OF_RANGE => "Out of Range",
42
- UNIMPLEMENTED => "Unimplemented",
43
- INTERNAL => "Internal",
44
- UNAVAILABLE => "Unavailable",
45
- DATA_LOSS => "Data Loss",
46
- UNAUTHENTICATED => "Unauthenticated"
45
+ CANCELLED => "CANCELLED",
46
+ UNKNOWN => "UNKNOWN",
47
+ INVALID_ARGUMENT => "INVALID_ARGUMENT",
48
+ DEADLINE_EXCEEDED => "DEADLINE_EXCEEDED",
49
+ NOT_FOUND => "NOT_FOUND",
50
+ ALREADY_EXISTS => "ALREADY_EXISTS",
51
+ PERMISSION_DENIED => "PERMISSION_DENIED",
52
+ RESOURCE_EXHAUSTED => "RESOURCE_EXHAUSTED",
53
+ FAILED_PRECONDITION => "FAILED_PRECONDITION",
54
+ ABORTED => "ABORTED",
55
+ OUT_OF_RANGE => "OUT_OF_RANGE",
56
+ UNIMPLEMENTED => "UNIMPLEMENTED",
57
+ INTERNAL => "INTERNAL",
58
+ UNAVAILABLE => "UNAVAILABLE",
59
+ DATA_LOSS => "DATA_LOSS",
60
+ UNAUTHENTICATED => "UNAUTHENTICATED"
47
61
  }.freeze
48
62
  end
49
63
  end
@@ -7,7 +7,7 @@
7
7
  module Protocol
8
8
  # @namespace
9
9
  module GRPC
10
- VERSION = "0.14.0"
10
+ VERSION = "0.16.0"
11
11
  end
12
12
  end
13
13
 
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.16.0
32
+
33
+ - Preserve timeout precision within the eight-digit wire limit, rounding up when necessary.
34
+ - Accept padded and unpadded binary metadata.
35
+ - **Breaking**: Error backtraces are no longer sent to clients by default. Pass `backtrace: true` to `Metadata.assign_status!` to explicitly enable them for debugging.
36
+ - Default request metadata to `application/grpc` and provide `Status.for_http_status` for responses without `grpc-status`.
37
+
38
+ ### v0.15.0
39
+
40
+ - **Breaking**: Removed `Protocol::GRPC::Status::DESCRIPTIONS`. Use `Protocol::GRPC::Status::NAMES` for canonical gRPC status names.
41
+
31
42
  ### v0.14.0
32
43
 
33
44
  - **Breaking**: Removed the unused `Protocol::GRPC::Call#cancel!` and `Protocol::GRPC::Call#cancelled?` methods.
@@ -67,14 +78,6 @@ Please see the [project releases](https://socketry.github.io/protocol-grpc/relea
67
78
  - **Breaking**: Removed `Call#response_headers` method. Use `call.response.headers` directly.
68
79
  - Added `RPC#streaming?` method to check if an RPC is streaming.
69
80
 
70
- ### v0.2.0
71
-
72
- - `RPC#method` is always defined (snake case).
73
-
74
- ### v0.1.0
75
-
76
- - Initial design.
77
-
78
81
  ## See Also
79
82
 
80
83
  - [async-grpc](https://github.com/socketry/async-grpc) — Asynchronous gRPC client and server implementation using this interface.
data/releases.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Releases
2
2
 
3
+ ## v0.16.0
4
+
5
+ - Preserve timeout precision within the eight-digit wire limit, rounding up when necessary.
6
+ - Accept padded and unpadded binary metadata.
7
+ - **Breaking**: Error backtraces are no longer sent to clients by default. Pass `backtrace: true` to `Metadata.assign_status!` to explicitly enable them for debugging.
8
+ - Default request metadata to `application/grpc` and provide `Status.for_http_status` for responses without `grpc-status`.
9
+
10
+ ## v0.15.0
11
+
12
+ - **Breaking**: Removed `Protocol::GRPC::Status::DESCRIPTIONS`. Use `Protocol::GRPC::Status::NAMES` for canonical gRPC status names.
13
+
3
14
  ## v0.14.0
4
15
 
5
16
  - **Breaking**: Removed the unused `Protocol::GRPC::Call#cancel!` and `Protocol::GRPC::Call#cancelled?` methods.
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.14.0
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -145,7 +145,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
145
145
  - !ruby/object:Gem::Version
146
146
  version: '0'
147
147
  requirements: []
148
- rubygems_version: 4.0.10
148
+ rubygems_version: 4.0.16
149
149
  specification_version: 4
150
150
  summary: Protocol abstractions for gRPC, built on top of protocol-http.
151
151
  test_files: []
metadata.gz.sig CHANGED
Binary file