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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +501 -0
- data/docs/design.md +504 -0
- data/docs/development.md +566 -0
- data/docs/grpc-web.md +165 -0
- data/docs/grpc.md +75 -0
- data/docs/rbs.md +469 -0
- data/docs/streaming.md +251 -0
- data/exe/protoc-gen-connectrpc-ruby +23 -0
- data/lib/connectrpc/bindings/descriptors/status.bin +15 -0
- data/lib/connectrpc/bindings/google_protobuf.rb +92 -0
- data/lib/connectrpc/cancellation.rb +220 -0
- data/lib/connectrpc/client.rb +260 -0
- data/lib/connectrpc/compression.rb +120 -0
- data/lib/connectrpc/deadline.rb +81 -0
- data/lib/connectrpc/error.rb +72 -0
- data/lib/connectrpc/generator/names.rb +105 -0
- data/lib/connectrpc/generator/rbs_renderer.rb +93 -0
- data/lib/connectrpc/generator/renderer.rb +115 -0
- data/lib/connectrpc/generator/runtime_names.rb +84 -0
- data/lib/connectrpc/generator/schema.rb +218 -0
- data/lib/connectrpc/generator/streaming_methods.rb +29 -0
- data/lib/connectrpc/generator.rb +42 -0
- data/lib/connectrpc/get_query.rb +159 -0
- data/lib/connectrpc/grpc.rb +200 -0
- data/lib/connectrpc/grpc_client.rb +23 -0
- data/lib/connectrpc/grpc_server.rb +158 -0
- data/lib/connectrpc/grpc_web.rb +131 -0
- data/lib/connectrpc/grpc_web_cors.rb +135 -0
- data/lib/connectrpc/grpc_web_io.rb +156 -0
- data/lib/connectrpc/grpc_web_server.rb +207 -0
- data/lib/connectrpc/instrumentation.rb +216 -0
- data/lib/connectrpc/message_io.rb +47 -0
- data/lib/connectrpc/metadata.rb +105 -0
- data/lib/connectrpc/method.rb +68 -0
- data/lib/connectrpc/protocol.rb +189 -0
- data/lib/connectrpc/server.rb +398 -0
- data/lib/connectrpc/streaming/client.rb +104 -0
- data/lib/connectrpc/streaming/client_execution.rb +87 -0
- data/lib/connectrpc/streaming/client_scope.rb +105 -0
- data/lib/connectrpc/streaming/framing.rb +123 -0
- data/lib/connectrpc/streaming/framing_io.rb +61 -0
- data/lib/connectrpc/streaming/method.rb +33 -0
- data/lib/connectrpc/streaming/server.rb +122 -0
- data/lib/connectrpc/streaming/server_execution.rb +88 -0
- data/lib/connectrpc/streaming/server_terminal.rb +70 -0
- data/lib/connectrpc/streaming/session.rb +143 -0
- data/lib/connectrpc/streaming/session_completion.rb +134 -0
- data/lib/connectrpc/streaming/session_io.rb +107 -0
- data/lib/connectrpc/streaming/session_messages.rb +119 -0
- data/lib/connectrpc/streaming/strategy.rb +95 -0
- data/lib/connectrpc/streaming/streams.rb +116 -0
- data/lib/connectrpc/streaming/terminal.rb +52 -0
- data/lib/connectrpc/transports/net_http.rb +246 -0
- data/lib/connectrpc/version.rb +5 -0
- data/lib/connectrpc.rb +38 -0
- data/rbs/google_protobuf_4_36/runtime.rbs +41 -0
- data/sig/connectrpc.rbs +772 -0
- data/sig/manifest.yaml +2 -0
- data/third_party/README.md +16 -0
- data/third_party/google/LICENSE +201 -0
- data/third_party/google/rpc/status.proto +48 -0
- data/third_party/protobuf-LICENSE +32 -0
- metadata +106 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module ConnectRPC
|
|
6
|
+
# One immutable policy per mount. Authentication, CSRF and outer middleware
|
|
7
|
+
# remain application-owned.
|
|
8
|
+
class GRPCWebCORS
|
|
9
|
+
BASE_REQUEST = %w[content-type x-grpc-web x-user-agent grpc-timeout accept].freeze
|
|
10
|
+
BASE_EXPOSE = %w[grpc-status grpc-message grpc-status-details-bin].freeze
|
|
11
|
+
TOKEN = /\A[!#$%&'*+.^_`|~0-9A-Za-z-]+\z/
|
|
12
|
+
private_constant :BASE_REQUEST, :BASE_EXPOSE, :TOKEN
|
|
13
|
+
|
|
14
|
+
attr_reader :origins, :credentials, :request_headers, :expose_headers
|
|
15
|
+
|
|
16
|
+
def initialize(origins:, credentials: false, request_headers: [], expose_headers: []) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
17
|
+
unless [true, false].include?(credentials) && origins.is_a?(Array) && origins.size <= 32 &&
|
|
18
|
+
origins.all? { |origin| valid_origin?(origin) } && origins.uniq.size == origins.size
|
|
19
|
+
raise ArgumentError, "CORS requires distinct exact HTTP(S) origins and boolean credentials"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
@origins = origins.map { |origin| origin.dup.freeze }.freeze
|
|
23
|
+
@credentials = credentials
|
|
24
|
+
@request_headers = (BASE_REQUEST + names(request_headers)).uniq.freeze
|
|
25
|
+
@expose_headers = (BASE_EXPOSE + names(expose_headers)).uniq.freeze
|
|
26
|
+
freeze
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def valid_origin?(origin) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
32
|
+
return false unless origin.is_a?(String) && origin.ascii_only? && origin.bytesize <= 2048
|
|
33
|
+
return false unless %r{\Ahttps?://[^/?#\s]+\z}.match?(origin)
|
|
34
|
+
|
|
35
|
+
uri = URI.parse(origin)
|
|
36
|
+
host = uri.host
|
|
37
|
+
port = ":#{uri.port}" unless uri.port == uri.default_port
|
|
38
|
+
!!(uri.is_a?(URI::HTTP) && host && !uri.userinfo && uri.to_s == origin &&
|
|
39
|
+
host == host.downcase && origin == "#{uri.scheme}://#{host}#{port}")
|
|
40
|
+
rescue URI::InvalidURIError
|
|
41
|
+
false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def names(values) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
45
|
+
unless values.is_a?(Array) && values.size <= 32 && values.all? do |value|
|
|
46
|
+
value.is_a?(String) && value.bytesize <= 128 && TOKEN.match?(value)
|
|
47
|
+
end
|
|
48
|
+
raise ArgumentError, "CORS header lists require at most 32 HTTP tokens of at most 128 bytes"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
normalized = values.map { |value| value.downcase.freeze }
|
|
52
|
+
raise ArgumentError, "CORS header names must be distinct" unless normalized.uniq.size == normalized.size
|
|
53
|
+
|
|
54
|
+
normalized
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class Server
|
|
59
|
+
def cors_response(path:, http_method:, headers:, response: nil) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
60
|
+
policy = @cors
|
|
61
|
+
return response unless policy && @protocol.include?(:grpc_web) && handles?(path)
|
|
62
|
+
|
|
63
|
+
return response if http_method != "OPTIONS" && request_protocol(headers, http_method) != :grpc_web
|
|
64
|
+
|
|
65
|
+
values = Protocol.headers(headers)
|
|
66
|
+
preflight = http_method == "OPTIONS" &&
|
|
67
|
+
(values.key?("origin") || values.key?("access-control-request-method"))
|
|
68
|
+
return response unless preflight || GRPCWeb.intent?(values)
|
|
69
|
+
|
|
70
|
+
origin = Protocol.single(values, "origin")
|
|
71
|
+
allowed = origin && policy.origins.include?(origin)
|
|
72
|
+
if preflight
|
|
73
|
+
evaluated = cors_preflight(values, policy, origin, allowed)
|
|
74
|
+
return evaluated unless response && evaluated.status == 204
|
|
75
|
+
|
|
76
|
+
decorated = merge_cors_fields(response.headers, evaluated.headers.except("vary"))
|
|
77
|
+
GRPCWeb.vary(decorated, *evaluated.headers.fetch("vary"))
|
|
78
|
+
return TransportResponse.new(status: response.status, headers: decorated, body: response.body)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
if values.key?("origin") && !allowed
|
|
82
|
+
return TransportResponse.new(status: 403, headers: { "vary" => %w[Origin Accept] }, body: "")
|
|
83
|
+
end
|
|
84
|
+
return unless response
|
|
85
|
+
|
|
86
|
+
# @type var fields: headers
|
|
87
|
+
fields = allowed && origin ? cors_fields(policy, origin) : {}
|
|
88
|
+
merged = merge_cors_fields(response.headers, fields)
|
|
89
|
+
GRPCWeb.vary(merged, "Origin", "Accept")
|
|
90
|
+
TransportResponse.new(status: response.status, headers: merged, body: response.body)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def cors_fields(policy, origin)
|
|
96
|
+
fields = { "access-control-allow-origin" => [origin],
|
|
97
|
+
"access-control-expose-headers" => [policy.expose_headers.join(", ")] }
|
|
98
|
+
fields["access-control-allow-credentials"] = ["true"] if policy.credentials
|
|
99
|
+
fields
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def merge_cors_fields(headers, fields)
|
|
103
|
+
normalized = Protocol.headers(headers)
|
|
104
|
+
if normalized.any? { |key, values| key.start_with?("access-control-") && fields[key] != values }
|
|
105
|
+
raise Error.new(:internal, "Conflicting application CORS policy"), cause: nil
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
normalized.merge(fields)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def cors_preflight(headers, policy, origin, allowed) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
112
|
+
controls = headers.select { |key, _| key == "origin" || key.start_with?("access-control-request-") }
|
|
113
|
+
if GRPCProtocol.field_size(controls) > 8192
|
|
114
|
+
return TransportResponse.new(status: 403, body: "", headers: {
|
|
115
|
+
"vary" => %w[Origin Access-Control-Request-Method Access-Control-Request-Headers]
|
|
116
|
+
})
|
|
117
|
+
end
|
|
118
|
+
requested = headers.fetch("access-control-request-headers", []).flat_map do |value|
|
|
119
|
+
value.split(",", -1).map(&:strip)
|
|
120
|
+
end
|
|
121
|
+
valid = GRPCProtocol.field_size(controls) <= 8192 && requested.size <= 32 &&
|
|
122
|
+
requested.all? { |value| /\A[!#$%&'*+.^_`|~0-9A-Za-z-]+\z/.match?(value) }
|
|
123
|
+
permitted = valid && allowed && Protocol.single(headers, "access-control-request-method") == "POST" &&
|
|
124
|
+
requested.all? { |value| policy.request_headers.include?(value.downcase) }
|
|
125
|
+
fields = { "vary" => %w[Origin Access-Control-Request-Method Access-Control-Request-Headers] }
|
|
126
|
+
if permitted && origin
|
|
127
|
+
fields.merge!(cors_fields(policy, origin))
|
|
128
|
+
fields["access-control-allow-methods"] = ["POST"]
|
|
129
|
+
fields["access-control-allow-headers"] = [policy.request_headers.join(", ")]
|
|
130
|
+
fields["access-control-max-age"] = ["0"]
|
|
131
|
+
end
|
|
132
|
+
TransportResponse.new(status: permitted ? 204 : 403, headers: fields, body: "")
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ConnectRPC
|
|
4
|
+
module GRPCWeb
|
|
5
|
+
# Decode each complete quartet independently: padding is permitted at every
|
|
6
|
+
# flush boundary, including inside a frame prefix or protobuf message.
|
|
7
|
+
class Input
|
|
8
|
+
def initialize(input, representation:, wire_limit:, headers:) # rubocop:disable Metrics/MethodLength
|
|
9
|
+
@input = input
|
|
10
|
+
@text = representation == :text
|
|
11
|
+
@encoded_limit = (@text ? 4 : 1) * (5 + wire_limit)
|
|
12
|
+
@decoded_limit = 5 + wire_limit
|
|
13
|
+
length = @length = Protocol.content_length!(headers["content-length"], invalid_code: :invalid_argument)
|
|
14
|
+
if length && length > @encoded_limit
|
|
15
|
+
raise Error.new(:resource_exhausted,
|
|
16
|
+
"Encoded request exceeds byte limit")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
@encoded = @decoded = 0
|
|
20
|
+
@carry = +"".b
|
|
21
|
+
@remainder = +"".b
|
|
22
|
+
@eof = false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def read(length, timeout: nil) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
26
|
+
while @remainder.empty? && !@eof
|
|
27
|
+
amount = @text ? [((length + 2) / 3) * 4, 16_384].min : [length, 16_384].min
|
|
28
|
+
bytes = @input&.read([amount, @encoded_limit - @encoded + 1].min, timeout: timeout)
|
|
29
|
+
if bytes.nil? || bytes.empty?
|
|
30
|
+
finish_input
|
|
31
|
+
else
|
|
32
|
+
@encoded += bytes.bytesize
|
|
33
|
+
Protocol.check_size!(bytes, amount)
|
|
34
|
+
if @encoded > @encoded_limit
|
|
35
|
+
raise Error.new(:resource_exhausted, "Encoded request exceeds byte limit"), cause: nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
@remainder = @text ? decode(bytes) : bytes
|
|
39
|
+
@decoded += @remainder.bytesize
|
|
40
|
+
if @decoded > @decoded_limit
|
|
41
|
+
raise Error.new(:resource_exhausted, "Framed request exceeds byte limit"), cause: nil
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
return if @remainder.empty?
|
|
46
|
+
|
|
47
|
+
result = @remainder.byteslice(0, length) #: String
|
|
48
|
+
@remainder = @remainder.byteslice(result.bytesize..) #: String
|
|
49
|
+
result
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def decode(bytes) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
55
|
+
bytes = @carry + bytes
|
|
56
|
+
count = bytes.bytesize - (bytes.bytesize % 4)
|
|
57
|
+
@carry = bytes.byteslice(count..) #: String
|
|
58
|
+
decoded = +"".b
|
|
59
|
+
offset = 0
|
|
60
|
+
while offset < count
|
|
61
|
+
group = bytes.byteslice(offset, 4) #: String
|
|
62
|
+
unless %r{\A(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)\z}.match?(group)
|
|
63
|
+
raise Error.new(:invalid_argument, "Malformed Web base64"), cause: nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
value = group.unpack1("m0") #: String
|
|
67
|
+
raise Error.new(:invalid_argument, "Noncanonical Web base64"), cause: nil unless [value].pack("m0") == group
|
|
68
|
+
|
|
69
|
+
decoded << value
|
|
70
|
+
offset += 4
|
|
71
|
+
end
|
|
72
|
+
decoded
|
|
73
|
+
rescue ArgumentError
|
|
74
|
+
raise Error.new(:invalid_argument, "Malformed Web base64"), cause: nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def finish_input
|
|
78
|
+
unless @carry.empty? && (@length.nil? || @length == @encoded)
|
|
79
|
+
raise Error.new(:invalid_argument, "Truncated Web request"), cause: nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
@eof = true
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
class Output
|
|
87
|
+
def initialize(output, representation)
|
|
88
|
+
@output = output
|
|
89
|
+
@text = representation == :text
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def write(bytes, timeout: nil)
|
|
93
|
+
size = @text ? 12_288 : 16_384
|
|
94
|
+
offset = 0
|
|
95
|
+
while offset < bytes.bytesize
|
|
96
|
+
fragment = bytes.byteslice(offset, size) #: String
|
|
97
|
+
@output.write(@text ? [fragment].pack("m0") : fragment, timeout: timeout)
|
|
98
|
+
offset += fragment.bytesize
|
|
99
|
+
end
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def finish(terminal:)
|
|
104
|
+
@output.finish(terminal: @text ? [terminal].pack("m0") : terminal)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def abort = @output.abort
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Portable unary materializes one bounded response, never a native stream.
|
|
111
|
+
class Aggregate
|
|
112
|
+
attr_reader :body
|
|
113
|
+
|
|
114
|
+
def initialize(limit)
|
|
115
|
+
@limit = limit
|
|
116
|
+
@body = +"".b
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def write(bytes, timeout: nil) # rubocop:disable Lint/UnusedMethodArgument -- synchronous bounded append
|
|
120
|
+
if @body.bytesize + bytes.bytesize > @limit
|
|
121
|
+
raise Error.new(:resource_exhausted,
|
|
122
|
+
"Aggregate Web response exceeds byte limit")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
@body << bytes
|
|
126
|
+
nil
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def finish(terminal:)
|
|
130
|
+
@terminal = terminal
|
|
131
|
+
nil
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def complete
|
|
135
|
+
terminal = @terminal
|
|
136
|
+
write(terminal) if terminal
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def abort
|
|
140
|
+
@body.clear
|
|
141
|
+
@terminal = nil
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
class PortableInput
|
|
147
|
+
def initialize(input)
|
|
148
|
+
@input = input
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def read(length, timeout: nil) # rubocop:disable Lint/UnusedMethodArgument -- portable cooperative I/O
|
|
152
|
+
@input&.read(length)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ConnectRPC
|
|
4
|
+
module GRPCWeb
|
|
5
|
+
class ServerState
|
|
6
|
+
attr_reader :headers, :publish, :lifecycle, :cancellation, :representation, :path
|
|
7
|
+
attr_accessor :context, :session, :error, :published
|
|
8
|
+
|
|
9
|
+
def initialize(path:, headers:, representation:, publish:, lifecycle:, cancellation:)
|
|
10
|
+
@path = path
|
|
11
|
+
@headers = headers
|
|
12
|
+
@representation = representation
|
|
13
|
+
@publish = publish
|
|
14
|
+
@lifecycle = lifecycle
|
|
15
|
+
@cancellation = cancellation
|
|
16
|
+
@published = false
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class Server # rubocop:disable Metrics/ClassLength
|
|
22
|
+
# HTTP-version-neutral publication. Hosts stage finish's terminal until the
|
|
23
|
+
# original application task has completed all outer cleanup successfully.
|
|
24
|
+
def call_grpc_web(path:, http_method:, headers:, input:, output:, publish:, capabilities: [:unary], # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
25
|
+
lifecycle: nil, cancellation: nil)
|
|
26
|
+
# @type var owned: bool?
|
|
27
|
+
owned = nil
|
|
28
|
+
# @type var error: Exception?
|
|
29
|
+
error = nil
|
|
30
|
+
GRPCWeb.capabilities!(capabilities)
|
|
31
|
+
values = Protocol.headers(headers)
|
|
32
|
+
early = cors_response(path: path, http_method: http_method, headers: values)
|
|
33
|
+
return early if early
|
|
34
|
+
|
|
35
|
+
lifecycle, owned = begin_native_processing(path, values, lifecycle, http_method)
|
|
36
|
+
work = lambda do
|
|
37
|
+
rejection = web_preflight(http_method, values, cancellation)
|
|
38
|
+
if rejection
|
|
39
|
+
lifecycle&.processing_finished(code: nil, http_status: rejection.status)
|
|
40
|
+
next cors_response(path: path, http_method: http_method, headers: values, response: rejection) || rejection
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
request = GRPCWeb.media(Protocol.single(values, "content-type")) #: GRPCWeb::representation
|
|
44
|
+
representation = GRPCWeb.representation(values, request) #: GRPCWeb::representation
|
|
45
|
+
state = GRPCWeb::ServerState.new(path: path, headers: values, representation: representation,
|
|
46
|
+
publish: publish, lifecycle: lifecycle, cancellation: cancellation)
|
|
47
|
+
process_grpc_web(state, input, output, request, capabilities)
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
lifecycle ? lifecycle.activate(&work) : work.call
|
|
51
|
+
rescue Exception => e # rubocop:disable Lint/RescueException -- discard staged status and preserve the original failure
|
|
52
|
+
error = e
|
|
53
|
+
lifecycle&.record_failure(e) unless owned.nil?
|
|
54
|
+
begin
|
|
55
|
+
output.abort unless owned.nil?
|
|
56
|
+
rescue StandardError => cleanup_error
|
|
57
|
+
lifecycle&.record_failure(cleanup_error)
|
|
58
|
+
end
|
|
59
|
+
raise
|
|
60
|
+
ensure
|
|
61
|
+
finish_native_processing(lifecycle, owned, error) unless owned.nil?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def web_preflight(http_method, headers, cancellation)
|
|
67
|
+
fields = { "vary" => ["Accept"] }
|
|
68
|
+
return bare(415, fields, cancellation: cancellation) unless @protocol.include?(:grpc_web)
|
|
69
|
+
return bare(405, fields.merge("allow" => ["POST"]), cancellation: cancellation) unless http_method == "POST"
|
|
70
|
+
|
|
71
|
+
representation = GRPCWeb.media(Protocol.single(headers, "content-type"))
|
|
72
|
+
unless representation
|
|
73
|
+
return bare(415, fields.merge("accept-post" => [GRPCWeb::CONTENT_TYPES.values.join(", ")]),
|
|
74
|
+
cancellation: cancellation)
|
|
75
|
+
end
|
|
76
|
+
return if GRPCWeb.representation(headers, representation)
|
|
77
|
+
|
|
78
|
+
bare(406, fields, cancellation: cancellation)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def call_grpc_web_unary(path, http_method, headers, input, lifecycle, cancellation) # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
|
|
82
|
+
# Separately padded writes can expand every raw byte by four. The bounded
|
|
83
|
+
# aggregate includes one data prefix and the separately staged terminal.
|
|
84
|
+
output = GRPCWeb::Aggregate.new(4 * (5 + @response_wire_limit + 5 + @grpc_status_wire_limit))
|
|
85
|
+
head = nil #: StreamHead?
|
|
86
|
+
response = call_grpc_web(path: path, http_method: http_method, headers: headers,
|
|
87
|
+
input: GRPCWeb::PortableInput.new(input), output: output,
|
|
88
|
+
lifecycle: lifecycle, cancellation: cancellation,
|
|
89
|
+
publish: ->(published) { head = published })
|
|
90
|
+
return response if response
|
|
91
|
+
|
|
92
|
+
output.complete
|
|
93
|
+
published = head #: StreamHead
|
|
94
|
+
TransportResponse.new(status: published.status, headers: published.headers, body: output.body)
|
|
95
|
+
rescue Exception # rubocop:disable Lint/RescueException -- discard staged status, preserve arbitrary cleanup failure
|
|
96
|
+
output&.abort
|
|
97
|
+
raise
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def process_grpc_web(state, input, output, request, capabilities)
|
|
101
|
+
prepared = GRPCWeb::Output.new(output, state.representation)
|
|
102
|
+
capture_stream_error(state) { execute_grpc_web(state, input, prepared, request, capabilities) }
|
|
103
|
+
render_grpc_web_terminal(state, prepared)
|
|
104
|
+
ensure
|
|
105
|
+
state.session&.close
|
|
106
|
+
state.context&.close
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def execute_grpc_web(state, input, output, representation, capabilities) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
110
|
+
GRPCProtocol.check_fields!(state.headers)
|
|
111
|
+
declaration = @methods[state.path]
|
|
112
|
+
allowed = if declaration.is_a?(Method)
|
|
113
|
+
capabilities.include?(:unary)
|
|
114
|
+
else
|
|
115
|
+
declaration.is_a?(ServerStreamMethod) && capabilities.include?(:server_stream) &&
|
|
116
|
+
@handlers[state.path]
|
|
117
|
+
end
|
|
118
|
+
raise Error.new(:unimplemented, "Unsupported Web method"), cause: nil unless allowed
|
|
119
|
+
|
|
120
|
+
GRPCProtocol.encoding(state.headers, request: true, enabled: :identity)
|
|
121
|
+
unless Protocol.identity_encoding?(state.headers)
|
|
122
|
+
raise Error.new(:unimplemented, "Web requires identity HTTP encoding"), cause: nil
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
context = state.context = web_context(state, declaration)
|
|
126
|
+
incoming = GRPCWeb::Input.new(input, representation: representation, wire_limit: @request_wire_limit,
|
|
127
|
+
headers: state.headers)
|
|
128
|
+
session = state.session = Streaming::Session.new(
|
|
129
|
+
binding: @binding, method: declaration, codec: :proto, state: context, input: incoming, output: output,
|
|
130
|
+
request_limit: @request_limit, response_limit: @response_limit,
|
|
131
|
+
request_wire_limit: @request_wire_limit, response_wire_limit: @response_wire_limit,
|
|
132
|
+
request_encoding: :identity, response_encoding: :identity,
|
|
133
|
+
terminal_wire_limit: @grpc_status_wire_limit, terminal_limit: @grpc_status_limit,
|
|
134
|
+
server: true, context: context, lifecycle: state.lifecycle,
|
|
135
|
+
publish: -> { publish_web_head(state) }, strategy: GRPCWeb::Strategy.new
|
|
136
|
+
)
|
|
137
|
+
message = session.receive
|
|
138
|
+
if message.nil? || incoming.read(1, timeout: context.deadline&.remaining)
|
|
139
|
+
raise Error.new(:unimplemented, "Expected exactly one request"), cause: nil
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
context.check!
|
|
143
|
+
if declaration.is_a?(Method)
|
|
144
|
+
session.send_message(@invoker.call(declaration, message, context))
|
|
145
|
+
else
|
|
146
|
+
handler = @handlers.fetch(state.path) #: _ServerStreamHandler[untyped, untyped]
|
|
147
|
+
streaming = context #: StreamingContext
|
|
148
|
+
handler.call(message, ResponseWriter.new(session, streaming), streaming)
|
|
149
|
+
end
|
|
150
|
+
context.check!
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def web_context(state, declaration = nil, headers: state.headers)
|
|
154
|
+
metadata = Protocol.metadata(headers)
|
|
155
|
+
state.lifecycle.request_metadata = Instrumentation.carrier(metadata) if state.lifecycle
|
|
156
|
+
options = { request_metadata: metadata, deadline: GRPCProtocol.request_deadline(headers),
|
|
157
|
+
cancellation: state.cancellation, lifecycle: state.lifecycle }
|
|
158
|
+
return Context.new(**options) unless declaration.is_a?(ServerStreamMethod)
|
|
159
|
+
|
|
160
|
+
StreamingContext.new(publish: ->(snapshot) { publish_web_snapshot(state, snapshot) }, **options)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def publish_web_head(state)
|
|
164
|
+
return if state.published
|
|
165
|
+
|
|
166
|
+
context = state.context #: Context
|
|
167
|
+
if context.is_a?(StreamingContext)
|
|
168
|
+
context.send(:publish_headers, check: false)
|
|
169
|
+
else
|
|
170
|
+
context.response_metadata.freeze
|
|
171
|
+
publish_web_snapshot(state, context.response_metadata)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def publish_web_snapshot(state, metadata) # rubocop:disable Metrics/AbcSize
|
|
176
|
+
fields = metadata.to_h.merge("content-type" => [GRPCWeb::CONTENT_TYPES.fetch(state.representation)],
|
|
177
|
+
"grpc-accept-encoding" => ["identity"])
|
|
178
|
+
GRPCWeb.vary(fields, "Accept")
|
|
179
|
+
response = TransportResponse.new(status: 200, headers: fields, body: "")
|
|
180
|
+
response = cors_response(path: state.path, http_method: "POST", headers: state.headers,
|
|
181
|
+
response: response) || response
|
|
182
|
+
GRPCProtocol.check_fields!(response.headers.merge(":status" => [response.status.to_s]))
|
|
183
|
+
state.publish.call(StreamHead.new(status: response.status, headers: response.headers))
|
|
184
|
+
state.published = true
|
|
185
|
+
nil
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def render_grpc_web_terminal(state, output) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
189
|
+
context = state.context ||= web_context(state, headers: {})
|
|
190
|
+
merge_stream_error_metadata(state.error, context)
|
|
191
|
+
codec = @status_codec #: _GRPCStatusCodec
|
|
192
|
+
bytes, effective = GRPCWeb.terminal(
|
|
193
|
+
state.error, stream_trailers(state.error, context),
|
|
194
|
+
codec: codec,
|
|
195
|
+
wire_limit: @grpc_status_wire_limit, limit: @grpc_status_limit,
|
|
196
|
+
on_error: lambda { |error|
|
|
197
|
+
state.lifecycle&.record_failure(error)
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
observe_stream_result(state, effective) if state.error
|
|
201
|
+
publish_web_head(state)
|
|
202
|
+
output.finish(terminal: bytes)
|
|
203
|
+
context.commit! unless state.error
|
|
204
|
+
observe_stream_result(state, effective) unless state.error
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|