connectrpc 0.1.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.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +501 -0
  4. data/docs/design.md +504 -0
  5. data/docs/development.md +566 -0
  6. data/docs/grpc-web.md +165 -0
  7. data/docs/grpc.md +75 -0
  8. data/docs/rbs.md +469 -0
  9. data/docs/streaming.md +251 -0
  10. data/exe/protoc-gen-connectrpc-ruby +23 -0
  11. data/lib/connectrpc/bindings/descriptors/status.bin +15 -0
  12. data/lib/connectrpc/bindings/google_protobuf.rb +92 -0
  13. data/lib/connectrpc/cancellation.rb +220 -0
  14. data/lib/connectrpc/client.rb +260 -0
  15. data/lib/connectrpc/compression.rb +120 -0
  16. data/lib/connectrpc/deadline.rb +81 -0
  17. data/lib/connectrpc/error.rb +72 -0
  18. data/lib/connectrpc/generator/names.rb +105 -0
  19. data/lib/connectrpc/generator/rbs_renderer.rb +93 -0
  20. data/lib/connectrpc/generator/renderer.rb +115 -0
  21. data/lib/connectrpc/generator/runtime_names.rb +84 -0
  22. data/lib/connectrpc/generator/schema.rb +218 -0
  23. data/lib/connectrpc/generator/streaming_methods.rb +29 -0
  24. data/lib/connectrpc/generator.rb +42 -0
  25. data/lib/connectrpc/get_query.rb +159 -0
  26. data/lib/connectrpc/grpc.rb +200 -0
  27. data/lib/connectrpc/grpc_client.rb +23 -0
  28. data/lib/connectrpc/grpc_server.rb +158 -0
  29. data/lib/connectrpc/grpc_web.rb +131 -0
  30. data/lib/connectrpc/grpc_web_cors.rb +135 -0
  31. data/lib/connectrpc/grpc_web_io.rb +156 -0
  32. data/lib/connectrpc/grpc_web_server.rb +207 -0
  33. data/lib/connectrpc/instrumentation.rb +216 -0
  34. data/lib/connectrpc/message_io.rb +47 -0
  35. data/lib/connectrpc/metadata.rb +105 -0
  36. data/lib/connectrpc/method.rb +68 -0
  37. data/lib/connectrpc/protocol.rb +189 -0
  38. data/lib/connectrpc/server.rb +398 -0
  39. data/lib/connectrpc/streaming/client.rb +104 -0
  40. data/lib/connectrpc/streaming/client_execution.rb +87 -0
  41. data/lib/connectrpc/streaming/client_scope.rb +105 -0
  42. data/lib/connectrpc/streaming/framing.rb +123 -0
  43. data/lib/connectrpc/streaming/framing_io.rb +61 -0
  44. data/lib/connectrpc/streaming/method.rb +33 -0
  45. data/lib/connectrpc/streaming/server.rb +122 -0
  46. data/lib/connectrpc/streaming/server_execution.rb +88 -0
  47. data/lib/connectrpc/streaming/server_terminal.rb +70 -0
  48. data/lib/connectrpc/streaming/session.rb +143 -0
  49. data/lib/connectrpc/streaming/session_completion.rb +134 -0
  50. data/lib/connectrpc/streaming/session_io.rb +107 -0
  51. data/lib/connectrpc/streaming/session_messages.rb +119 -0
  52. data/lib/connectrpc/streaming/strategy.rb +95 -0
  53. data/lib/connectrpc/streaming/streams.rb +116 -0
  54. data/lib/connectrpc/streaming/terminal.rb +52 -0
  55. data/lib/connectrpc/transports/net_http.rb +246 -0
  56. data/lib/connectrpc/version.rb +5 -0
  57. data/lib/connectrpc.rb +38 -0
  58. data/rbs/google_protobuf_4_36/runtime.rbs +41 -0
  59. data/sig/connectrpc.rbs +772 -0
  60. data/sig/manifest.yaml +2 -0
  61. data/third_party/README.md +16 -0
  62. data/third_party/google/LICENSE +201 -0
  63. data/third_party/google/rpc/status.proto +48 -0
  64. data/third_party/protobuf-LICENSE +32 -0
  65. metadata +106 -0
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConnectRPC
4
+ module Streaming
5
+ # Read and write guards release only their own accepted operation.
6
+ class Direction
7
+ attr_reader :busy
8
+
9
+ def initialize
10
+ @busy = false
11
+ end
12
+
13
+ def run
14
+ entered = false
15
+ begin
16
+ raise StreamStateError, "Overlapping stream operation" if @busy
17
+
18
+ @busy = entered = true
19
+ yield
20
+ ensure
21
+ @busy = false if entered
22
+ end
23
+ end
24
+ end
25
+
26
+ class Session
27
+ attr_reader :trailers, :received, :sent
28
+
29
+ def initialize(binding:, method:, codec:, state:, input:, output:, request_limit:, response_limit:,
30
+ request_wire_limit:, response_wire_limit:, request_encoding:, response_encoding:,
31
+ terminal_wire_limit:, terminal_limit:, server: false, context: nil, lifecycle: nil,
32
+ accept_encoding: [:identity], strategy: ConnectStrategy.new, publish: nil)
33
+ @strategy = strategy
34
+ @publish = publish
35
+ initialize_owner(binding, method, codec, state)
36
+ initialize_channels(input, output, server, context, lifecycle)
37
+ @accept_encoding = accept_encoding
38
+ initialize_messages(request_limit, response_limit)
39
+ initialize_framing(request_wire_limit, response_wire_limit, request_encoding, response_encoding,
40
+ terminal_wire_limit: terminal_wire_limit, terminal_limit: terminal_limit)
41
+ end
42
+
43
+ def abandoned? = @abandoned
44
+ def consumed? = @eof
45
+ def send_failure?(error) = error.equal?(@send_failure)
46
+
47
+ def owned_error?(error)
48
+ error.equal?(@reading_error) || error.equal?(@writing_error) || error.equal?(@abandon_error)
49
+ end
50
+
51
+ def close
52
+ @closed = true
53
+ nil
54
+ end
55
+
56
+ private
57
+
58
+ def initialize_owner(binding, method, codec, state)
59
+ @binding = binding
60
+ @method = method
61
+ @codec = codec
62
+ @state = state
63
+ @thread = Thread.current
64
+ @scheduler = Fiber.scheduler
65
+ @closed = @send_closed = @eof = @abandoned = false
66
+ end
67
+
68
+ def initialize_channels(input, output, server, context, lifecycle)
69
+ @input = input
70
+ @output = output
71
+ @server = server
72
+ @context = context
73
+ @lifecycle = lifecycle
74
+ @reading = Direction.new
75
+ @writing = Direction.new
76
+ end
77
+
78
+ def initialize_messages(request_limit, response_limit)
79
+ @received = @sent = 0
80
+ @read_limit = @server ? request_limit : response_limit
81
+ @write_limit = @server ? response_limit : request_limit
82
+ @read_type = @server ? @method.request_type : @method.response_type
83
+ @write_type = @server ? @method.response_type : @method.request_type
84
+ end
85
+
86
+ def initialize_framing(request_wire, response_wire, request_encoding, response_encoding,
87
+ terminal_wire_limit:, terminal_limit:)
88
+ initialize_reader(request_wire, response_wire, request_encoding, response_encoding,
89
+ terminal_wire_limit: terminal_wire_limit, terminal_limit: terminal_limit)
90
+ initialize_writer(request_wire, response_wire, request_encoding, response_encoding,
91
+ terminal_wire_limit: terminal_wire_limit, terminal_limit: terminal_limit)
92
+ end
93
+
94
+ def initialize_reader(request_wire, response_wire, request_encoding, response_encoding,
95
+ terminal_wire_limit:, terminal_limit:)
96
+ @reader = @strategy.framing.new(input: self, request: @server, limit: @read_limit, state: @state,
97
+ wire_limit: @server ? request_wire : response_wire,
98
+ encoding: @server ? request_encoding : response_encoding,
99
+ terminal_wire_limit:, terminal_limit:,
100
+ on_error: ->(error) { @lifecycle&.record_failure(error) })
101
+ end
102
+
103
+ def initialize_writer(request_wire, response_wire, request_encoding, response_encoding,
104
+ terminal_wire_limit:, terminal_limit:)
105
+ @writer = @strategy.framing.new(input: nil, output: self, request: !@server, limit: @write_limit, state: @state,
106
+ wire_limit: @server ? response_wire : request_wire,
107
+ encoding: @server ? response_encoding : request_encoding,
108
+ terminal_wire_limit: terminal_wire_limit,
109
+ terminal_limit: terminal_limit,
110
+ on_error: ->(error) { @lifecycle&.record_failure(error) })
111
+ end
112
+
113
+ def check_open!
114
+ raise StreamStateError, "Stream scope is closed" if @closed || @abandoned
115
+ return if Thread.current.equal?(@thread) && Fiber.scheduler.equal?(@scheduler)
116
+
117
+ raise StreamStateError, "Use the stream's owning reactor and thread"
118
+ end
119
+
120
+ def operation(direction)
121
+ guard = direction == :reading ? @reading : @writing
122
+ guard.run do
123
+ check_open!
124
+ @state.check!
125
+ yield
126
+ end
127
+ rescue StandardError => e
128
+ record_operation_failure(e, direction)
129
+ raise
130
+ end
131
+
132
+ def record_operation_failure(error, direction)
133
+ # Independent directions retain one failure each across application unwind.
134
+ case direction
135
+ when :reading then @reading_error = error
136
+ when :writing then @writing_error = error
137
+ when :abandon then @abandon_error = error
138
+ end
139
+ @lifecycle&.record_failure(error)
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConnectRPC
4
+ module Streaming
5
+ class Session
6
+ # Recovery runs only after the scope relinquishes normal consumption.
7
+ # It cannot steal a live reader, decode skipped data or resume the endpoint.
8
+ def recover_send_failure
9
+ return @remote_error if @eof
10
+ return unless recovery_available?
11
+
12
+ @recovering = true
13
+ @recovery_expires ||= now + RECOVERY_SECONDS
14
+ @recovery_remaining ||= RECOVERY_LIMIT
15
+ operation(:reading) { recover_terminal }
16
+ rescue StandardError => e
17
+ @lifecycle&.record_failure(e)
18
+ nil
19
+ end
20
+
21
+ private
22
+
23
+ def recovery_available? = !@reading.busy && !@server && !@abandoned
24
+
25
+ def recover_terminal # rubocop:disable Metrics/MethodLength
26
+ leading
27
+ loop do
28
+ frame = @reader.read_frame(skip_data: true)
29
+ unless frame
30
+ return recovered_grpc_error if @strategy.grpc?
31
+
32
+ return
33
+ end
34
+ return recovered_error(frame.last) if frame.first.anybits?(2)
35
+
36
+ @received += 1
37
+ return if !@strategy.grpc? && @method.is_a?(ClientStreamMethod) && @received > 1
38
+ end
39
+ end
40
+
41
+ def recovered_grpc_error
42
+ strategy = @strategy #: GRPCProtocol::Strategy
43
+ exchange = @input #: _GRPCStreamExchange
44
+ return unless strategy.recoverable?(exchange)
45
+
46
+ accept_grpc_terminal
47
+ nil
48
+ rescue Error => e
49
+ return e if e.equal?(@remote_error) && @eof
50
+
51
+ raise
52
+ end
53
+
54
+ def accept_grpc_terminal
55
+ strategy = @strategy #: GRPCProtocol::Strategy
56
+ exchange = @input #: _GRPCStreamExchange
57
+ @remote_error, @trailers = strategy.terminal(exchange, @headers || Metadata.new)
58
+ validate_terminal_count
59
+ expires = @recovery_expires || (now + Deadline::MAX_NATIVE_WAIT)
60
+ finish_transport(expires)
61
+ @eof = true
62
+ error = @remote_error
63
+ raise error, cause: nil if error
64
+
65
+ nil
66
+ end
67
+
68
+ def recovered_error(bytes)
69
+ accept_terminal(bytes)
70
+ nil
71
+ rescue Error => e
72
+ return e if e.equal?(@remote_error) && @eof
73
+
74
+ raise
75
+ end
76
+
77
+ def accept_terminal(bytes)
78
+ @remote_error, @trailers = parse_terminal(bytes)
79
+ validate_terminal_count
80
+ expires = @recovery_expires || (now + TAIL_SECONDS)
81
+ verify_tail(expires)
82
+ finish_transport(expires)
83
+ @eof = true
84
+ error = @remote_error
85
+ raise error, cause: nil if error
86
+
87
+ nil
88
+ end
89
+
90
+ def parse_terminal(bytes)
91
+ Streaming.parse_terminal(bytes, metadata: @headers || Metadata.new,
92
+ on_error: ->(error) { @lifecycle&.record_failure(error) })
93
+ end
94
+
95
+ def validate_terminal_count
96
+ return unless !@remote_error && @strategy.single_response?(@method) && @received != 1
97
+
98
+ raise Error.new(:unimplemented, "Expected exactly one response"), cause: nil
99
+ end
100
+
101
+ def verify_tail(expires)
102
+ extra = read(1, timeout: completion_timeout(expires))
103
+ return unless extra && !extra.empty?
104
+
105
+ raise Error.new(:internal, "Body bytes after streaming terminal"), cause: nil
106
+ end
107
+
108
+ def finish_transport(expires)
109
+ exchange = @input #: _StreamExchange
110
+ output = @output #: _StreamExchange
111
+ output.close_send
112
+ @send_closed = true
113
+ exchange.finish(timeout: completion_timeout(expires))
114
+ @state.check!
115
+ completion_timeout(expires)
116
+ nil
117
+ end
118
+
119
+ def completion_timeout(expires)
120
+ remaining = expires - now
121
+ raise Error.new(:unavailable, "Streaming completion policy expired"), cause: nil unless remaining.positive?
122
+
123
+ remaining
124
+ end
125
+
126
+ def recovery_timeout
127
+ expires = @recovery_expires #: Float
128
+ completion_timeout(expires)
129
+ end
130
+
131
+ def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConnectRPC
4
+ module Streaming
5
+ class Session
6
+ def write(bytes)
7
+ check_open!
8
+ write_chunk(bytes)
9
+ check_open!
10
+ nil
11
+ rescue StandardError => e
12
+ record_send_failure(e)
13
+ end
14
+
15
+ def read(length, timeout: nil)
16
+ check_open!
17
+ timeout = recovery_read_timeout(length) if @recovery_expires
18
+ deadline = @context&.deadline
19
+ timeout = deadline.remaining if @server && deadline
20
+ bytes = @input.read(length, timeout: @strategy.wait(timeout))
21
+ check_open!
22
+ count_recovery_bytes(bytes)
23
+ bytes
24
+ end
25
+
26
+ private
27
+
28
+ def write_chunk(bytes)
29
+ if @server
30
+ output = @output #: _StreamingOutput
31
+ output.write(bytes, timeout: @strategy.wait(@context&.deadline&.remaining))
32
+ else
33
+ @output.write(bytes)
34
+ end
35
+ end
36
+
37
+ def record_send_failure(error)
38
+ @lifecycle&.record_failure(error)
39
+ @state.check!
40
+ if error.is_a?(IOError) || error.is_a?(SystemCallError)
41
+ failure = @send_failure = Error.new(:unavailable, "Streaming write failed")
42
+ raise failure, cause: nil
43
+ end
44
+ @send_failure = error if error.is_a?(Error) && error.code == "unavailable"
45
+ raise error
46
+ end
47
+
48
+ def recovery_read_timeout(length)
49
+ remaining = @recovery_remaining #: Integer
50
+ raise Error.new(:resource_exhausted, "Streaming recovery byte limit"), cause: nil if length > remaining
51
+
52
+ recovery_timeout
53
+ end
54
+
55
+ def count_recovery_bytes(bytes)
56
+ remaining = @recovery_remaining
57
+ @recovery_remaining = remaining - bytes.bytesize if remaining && bytes
58
+ end
59
+
60
+ def leading
61
+ snapshot = @headers
62
+ return snapshot if snapshot
63
+
64
+ @headers = if @server
65
+ @context #: StreamingContext
66
+ .request_metadata.freeze
67
+ else
68
+ response_metadata
69
+ end
70
+ end
71
+
72
+ def response_metadata # rubocop:disable Metrics/MethodLength
73
+ exchange = @input #: _StreamExchange
74
+ strategy = @strategy
75
+ if strategy.is_a?(GRPCProtocol::Strategy)
76
+ grpc = @input #: _GRPCStreamExchange
77
+ return strategy.leading(grpc, @reader, @accept_encoding, @recovery_expires ? recovery_timeout : nil,
78
+ input: self)
79
+ end
80
+ head = @recovery_expires ? exchange.response_head(timeout: recovery_timeout) : exchange.response_head
81
+ validate_response_status(head.status)
82
+ values = Protocol.headers(head.headers)
83
+ validate_response_headers(values)
84
+ Protocol.metadata(values).freeze
85
+ end
86
+
87
+ def validate_response_status(status)
88
+ return if status == 200
89
+
90
+ raise Error.new(Error::HTTP_FALLBACK.fetch(status, "unknown"), "HTTP #{status}"), cause: nil
91
+ end
92
+
93
+ def validate_response_headers(values)
94
+ unless Streaming.codec(Protocol.single(values, "content-type")) == @codec
95
+ raise Error.new(:internal, "Unexpected streaming response content type"), cause: nil
96
+ end
97
+
98
+ encoding = Protocol.content_encoding(Streaming.coding_headers(values))
99
+ unless encoding && @accept_encoding.include?(encoding)
100
+ raise Error.new(:internal, "Unsupported streaming response encoding"), cause: nil
101
+ end
102
+
103
+ @reader.encoding = encoding
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConnectRPC
4
+ module Streaming
5
+ class Session
6
+ def remote_completed?(error)
7
+ @strategy.grpc? && @eof && error.equal?(@remote_error)
8
+ end
9
+
10
+ def headers
11
+ @headers || operation(:reading) { leading }
12
+ end
13
+
14
+ def receive
15
+ operation(:reading) { receive_message }
16
+ end
17
+
18
+ def send_message(message)
19
+ operation(:writing) do
20
+ validate_send(message)
21
+ encode_message(message)
22
+ end
23
+ end
24
+
25
+ def close_send
26
+ operation(:writing) do
27
+ return nil if @send_closed
28
+
29
+ @send_closed = true
30
+ output = @output #: _StreamExchange
31
+ output.close_send
32
+ nil
33
+ end
34
+ end
35
+
36
+ def abandon
37
+ check_open!
38
+ @abandoned = true
39
+ @output.abort
40
+ nil
41
+ rescue StandardError => e
42
+ record_operation_failure(e, :abandon)
43
+ raise
44
+ end
45
+
46
+ private
47
+
48
+ def receive_message
49
+ raise StreamStateError, "Normal response consumption was relinquished" if @recovering
50
+
51
+ return completed_receive if @eof
52
+
53
+ leading
54
+ frame = @reader.read_frame
55
+ return receive_eof unless frame
56
+ return accept_terminal(frame.last) if frame.first.anybits?(2)
57
+
58
+ decode_message(frame.last)
59
+ end
60
+
61
+ def completed_receive
62
+ error = @remote_error
63
+ raise error, cause: nil if error
64
+
65
+ nil
66
+ end
67
+
68
+ def receive_eof
69
+ return accept_grpc_terminal unless @server || !@strategy.grpc?
70
+
71
+ raise Error.new(:internal, "Missing streaming terminal"), cause: nil unless @server
72
+
73
+ @eof = true
74
+ nil
75
+ end
76
+
77
+ def decode_message(bytes)
78
+ @received += 1
79
+ validate_response_count unless @strategy.grpc?
80
+ message = @binding.decode(bytes, type: @read_type, codec: @codec)
81
+ @state.check!
82
+ raise Error.new(:internal, "Binding decoded a nil streaming message"), cause: nil if message.nil?
83
+
84
+ message
85
+ rescue CodecError => e
86
+ @lifecycle&.record_failure(e)
87
+ raise Error.new(@strategy.decode_error(@server), "Cannot decode streaming message"), cause: nil
88
+ end
89
+
90
+ def validate_response_count
91
+ return unless !@server && @method.is_a?(ClientStreamMethod) && @received > 1
92
+
93
+ raise Error.new(:unimplemented, "Expected exactly one response"), cause: nil
94
+ end
95
+
96
+ def validate_send(message)
97
+ raise StreamStateError, "Request sending is closed" if @send_closed
98
+ raise Error.new(:invalid_argument, "Nil is not a streaming message"), cause: nil if message.nil?
99
+ return unless !@server && @method.is_a?(ServerStreamMethod) && @sent.positive?
100
+
101
+ raise StreamStateError, "Server streaming accepts one request"
102
+ end
103
+
104
+ def encode_message(message) # rubocop:disable Metrics/MethodLength
105
+ bytes = @binding.encode(message, type: @write_type, codec: @codec)
106
+ @state.check!
107
+ Protocol.check_size!(bytes, @write_limit)
108
+ publisher = @publish
109
+ publisher ? publisher.call : @context&.send(:publish_headers)
110
+ @writer.emit(bytes)
111
+ @sent += 1
112
+ nil
113
+ rescue CodecError => e
114
+ @lifecycle&.record_failure(e)
115
+ raise Error.new(@server ? :internal : :invalid_argument, "Cannot encode streaming message"), cause: nil
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConnectRPC
4
+ module Streaming
5
+ # Protocol policy alone varies; Session owns all scope and I/O arbitration.
6
+ class ConnectStrategy
7
+ def grpc? = false
8
+ def framing = Framing
9
+ def wait(timeout) = timeout
10
+ def single_response?(method) = method.is_a?(ClientStreamMethod)
11
+ def decode_error(server) = server ? :invalid_argument : :internal
12
+ end
13
+ end
14
+
15
+ module GRPCProtocol
16
+ class Framing < Streaming::Framing
17
+ private
18
+
19
+ def validate_flags(flags)
20
+ protocol_error("Invalid gRPC envelope flags") unless [0, 1].include?(flags)
21
+ protocol_error("Unnegotiated compression") if flags == 1 && @encoding != :gzip
22
+ end
23
+
24
+ def protocol_error(message)
25
+ raise Error.new(:internal, message), cause: nil
26
+ end
27
+ end
28
+
29
+ class Strategy
30
+ def initialize(codec:, wire_limit:, limit:, on_error: nil)
31
+ @on_error = on_error
32
+ @codec = codec
33
+ @wire_limit = wire_limit
34
+ @limit = limit
35
+ end
36
+
37
+ def grpc? = true
38
+ def framing = Framing
39
+ def wait(timeout) = [timeout || Deadline::MAX_NATIVE_WAIT, Deadline::MAX_NATIVE_WAIT].min
40
+ def single_response?(method) = method.is_a?(Method) || method.is_a?(ClientStreamMethod)
41
+ def decode_error(_server) = :internal
42
+
43
+ def leading(exchange, reader, accept_encoding, timeout, input:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
44
+ head = @head = exchange.response_head(timeout: wait(timeout))
45
+ values = Protocol.headers(head.headers)
46
+ GRPCProtocol.check_fields!(values)
47
+ reject_media(input, exchange, head, timeout) unless GRPCProtocol.media?(Protocol.single(values, "content-type"))
48
+ if !head.end_stream && values.key?("grpc-status") && GRPCProtocol.status_value(values).nil?
49
+ raise Error.new(:unknown, "Malformed initial grpc-status"), cause: nil
50
+ end
51
+
52
+ coding = GRPCProtocol.encoding(values)
53
+ raise Error.new(:internal, "Unadvertised gRPC encoding"), cause: nil unless accept_encoding.include?(coding)
54
+
55
+ reader.encoding = coding
56
+ head.end_stream ? Metadata.new.freeze : Protocol.metadata(values).freeze
57
+ end
58
+
59
+ def reject_media(input, exchange, head, timeout) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
60
+ if head.end_stream
61
+ terminal = head.headers
62
+ else
63
+ loop do
64
+ bytes = input.read(Streaming::FRAGMENT_SIZE, timeout: wait(timeout))
65
+ break if bytes.nil? || bytes.empty?
66
+ end
67
+ terminal = exchange.response_trailers || {}
68
+ end
69
+ GRPCProtocol.check_fields!(terminal, @wire_limit)
70
+ code = if terminal.key?("grpc-status") || head.headers.key?("grpc-status")
71
+ "internal"
72
+ else
73
+ Error::HTTP_FALLBACK.fetch(head.status, "unknown")
74
+ end
75
+ raise Error.new(code, "Unexpected gRPC response content type"), cause: nil
76
+ end
77
+
78
+ def recoverable?(exchange)
79
+ head = @head #: GRPCHead
80
+ fields = head.end_stream ? head.headers : exchange.response_trailers
81
+ return false unless fields
82
+
83
+ number = GRPCProtocol.status_value(fields)
84
+ !!number&.positive?
85
+ end
86
+
87
+ def terminal(exchange, metadata)
88
+ head = @head #: GRPCHead
89
+ fields = head.end_stream ? head.headers : exchange.response_trailers
90
+ GRPCProtocol.parse_status(fields || {}, codec: @codec, http_status: head.status, metadata: metadata,
91
+ wire_limit: @wire_limit, limit: @limit, on_error: @on_error)
92
+ end
93
+ end
94
+ end
95
+ end