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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 444e77cbd9ce738f5babb41ea25029e57c004738282f18c16dc102f413b96125
|
|
4
|
+
data.tar.gz: 0bfe9f9329071885b05f80d6d768b30285332777e69f61b0cbe7bed0fbb6f424
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1374476f0d1b5e4564a639dfff93938d9fb6d379199d090001d081c83f76851a7590dfafacd88fc50a8276e0636a44c0a5c62ae459dc926985933a3aea7ae89f
|
|
7
|
+
data.tar.gz: 811d31f2fa60d4be74906e1c7b121f0928e7158b18fe2ee1ce546571511f5624bbb41f4d2b9055d1a2ef1873bebb998bab80bd5826c7e3a4d8c01ec82dd7030f
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jim Myhrberg
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
# connectrpc-ruby
|
|
2
|
+
|
|
3
|
+
An unofficial, experimental Ruby implementation of typed
|
|
4
|
+
[Connect](https://connectrpc.com/docs/protocol/) unary and native streaming RPCs. It provides a
|
|
5
|
+
configurable client, a Net::HTTP transport, and host-neutral servers for reusable
|
|
6
|
+
Ruby handlers. The optional [connectrpc-rack gem](gems/connectrpc-rack/README.md)
|
|
7
|
+
provides Rack middleware. It is not production ready.
|
|
8
|
+
|
|
9
|
+
Connect supports unary POST and opt-in safe unary GET, with identity or opt-in
|
|
10
|
+
gzip encoding and `application/proto` or protobuf `application/json`. The optional
|
|
11
|
+
[connectrpc-on-rails gem](gems/connectrpc-on-rails/README.md) runs generated unary
|
|
12
|
+
handlers through native Rails controllers.
|
|
13
|
+
|
|
14
|
+
Native Connect streaming uses the optional Async transport and Async/Falcon
|
|
15
|
+
HTTP/2 hosts. Falcon also serves Connect HTTP/1.1 server and client streaming.
|
|
16
|
+
The explicit Async `HTTP1Transport` supports unary, server streaming and client
|
|
17
|
+
streaming. See the [streaming guide](docs/streaming.md), including its one-second
|
|
18
|
+
HTTP tail policy for native Connect streaming clients.
|
|
19
|
+
|
|
20
|
+
Native [gRPC](docs/grpc.md) supports unary, server streaming, client streaming
|
|
21
|
+
and bidi through the same core declarations and Async/Falcon HTTP/2 engine,
|
|
22
|
+
with protobuf, identity/gzip, h2c, verified TLS and mTLS. Explicit server
|
|
23
|
+
[gRPC-Web](docs/grpc-web.md) supports unary and native server streaming within
|
|
24
|
+
its binary/text, host and compression limits. Connect remains the default.
|
|
25
|
+
Faraday remains unsupported. The API is experimental.
|
|
26
|
+
|
|
27
|
+
The [supported 0.x contract](docs/support.md) defines the target protocol, host,
|
|
28
|
+
platform and API compatibility boundaries. Adoption still depends on the open
|
|
29
|
+
[#26 findings](https://github.com/jimeh/connectrpc-ruby/issues/26) and release
|
|
30
|
+
qualification; current artifacts are not declared supported or production ready.
|
|
31
|
+
|
|
32
|
+
## Try the localhost example
|
|
33
|
+
|
|
34
|
+
Install [Mise](https://mise.jdx.dev/) 2026.9.14 or newer and the
|
|
35
|
+
[Ruby source-build prerequisites](https://github.com/rbenv/ruby-build/wiki#suggested-build-environment),
|
|
36
|
+
then run:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
mise run setup
|
|
40
|
+
mise run example
|
|
41
|
+
mise run verify
|
|
42
|
+
mise run rack:verify
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
[The example](examples/unary.rb) starts Rack on an ephemeral loopback port,
|
|
46
|
+
calls a typed handler through real Net::HTTP in both codecs, prints the results,
|
|
47
|
+
and closes the host and its workers. Its schema comes from the shared
|
|
48
|
+
[descriptor fixture](spec/fixtures/echo.proto). Go is needed only for interop
|
|
49
|
+
and final verification, and those tasks install its project pin separately.
|
|
50
|
+
|
|
51
|
+
To run a server and call it from another terminal, see the
|
|
52
|
+
[runnable examples](examples/README.md): Rack, Rails, Async and Falcon servers
|
|
53
|
+
with NetHTTP and Async clients, including mutual TLS.
|
|
54
|
+
|
|
55
|
+
## Generate services from protobuf
|
|
56
|
+
|
|
57
|
+
`protoc-gen-connectrpc-ruby` generates mixed unary/streaming declarations, thin clients
|
|
58
|
+
and handler maps. Supply google-protobuf `>= 4.36.2, < 5` for the plugin and
|
|
59
|
+
Google message integration. The core gem still has zero runtime dependencies.
|
|
60
|
+
Run the complete `.proto` -> generated messages/service -> ephemeral localhost
|
|
61
|
+
example with:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
mise run example:generated
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
It compiles [greeting.proto](examples/proto/greeting.proto) with protoc 36.2,
|
|
68
|
+
then [calls the generated client](examples/generated.rb) in binary and JSON,
|
|
69
|
+
including typed deliberate error details. To use the installed gem's plugin
|
|
70
|
+
in your own project, with `protoc-gen-connectrpc-ruby` on PATH:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
mkdir -p generated
|
|
74
|
+
protoc -I examples/proto --ruby_out=generated --connectrpc-ruby_out=generated greeting.proto
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
This creates official `greeting_pb.rb` and `greeting_connect.rb`. Add `generated`
|
|
78
|
+
to the Ruby load path. The service API is:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
require "greeting_connect"
|
|
82
|
+
require "connectrpc/bindings/google_protobuf"
|
|
83
|
+
|
|
84
|
+
service = Greeting::V1::GreetingService
|
|
85
|
+
binding = ConnectRPC::Bindings::GoogleProtobuf.new
|
|
86
|
+
core = ConnectRPC::Client.new(base_url: "http://127.0.0.1:8080", binding: binding, codec: :json)
|
|
87
|
+
client = service::Client.new(client: core)
|
|
88
|
+
response = client.greet(Greeting::V1::Greeting.new(name: "Ruby"), timeout: 2)
|
|
89
|
+
|
|
90
|
+
# Supply application callables accepting (request, context).
|
|
91
|
+
handlers = service.handlers(greet: greet_handler, fail: fail_handler)
|
|
92
|
+
server = ConnectRPC::Server.new(services: [service::SERVICE], handlers: handlers, binding: binding)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`greet_handler` and `fail_handler` above are application-defined callables; the
|
|
96
|
+
[runnable example](examples/generated.rb) defines both and hosts the Rack app.
|
|
97
|
+
`GREET` and `FAIL` are frozen `ConnectRPC::Method` constants; `SERVICE` is reusable.
|
|
98
|
+
Wrapper calls return the existing `Response` and forward keyword options exactly,
|
|
99
|
+
including omission of `timeout:` versus explicit `timeout: nil`. Binding, transport,
|
|
100
|
+
codec, URL and limits belong to the configured core client. `handlers` requires
|
|
101
|
+
all declared RPC keywords and validates callability; use the existing Server
|
|
102
|
+
handler map directly for partial/custom registration.
|
|
103
|
+
|
|
104
|
+
Buf 1.75.0 local generation is tested alongside official message generation:
|
|
105
|
+
|
|
106
|
+
```yaml
|
|
107
|
+
# buf.gen.yaml, for a Buf module containing greeting.proto
|
|
108
|
+
version: v2
|
|
109
|
+
plugins:
|
|
110
|
+
- protoc_builtin: ruby
|
|
111
|
+
out: generated
|
|
112
|
+
- local: protoc-gen-connectrpc-ruby
|
|
113
|
+
out: generated
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Run `buf generate` in that module with pinned protoc on PATH. For this checkout,
|
|
117
|
+
`mise run generation:check` runs both actual tools with explicit local paths,
|
|
118
|
+
compares glue bytes and exercises both generated message trees.
|
|
119
|
+
|
|
120
|
+
Proto2 and proto3, including optional presence and mixed unary/streaming
|
|
121
|
+
services, are supported. Editions and unsupported plugin parameters are rejected.
|
|
122
|
+
Default output is `dir/foo_connect.rb` for service-bearing `dir/foo.proto`.
|
|
123
|
+
Message generation remains official `--ruby_out`. Set `rbs=true` to also emit
|
|
124
|
+
`dir/foo_connect.rbs`.
|
|
125
|
+
|
|
126
|
+
The gem ships optional core signatures; the [RBS guide](docs/rbs.md) covers
|
|
127
|
+
official message `--rbs_out`, explicit protobuf bridge selection and strict Steep
|
|
128
|
+
configuration. Ruby module
|
|
129
|
+
placement follows official package/`ruby_package` naming; RPC methods use the
|
|
130
|
+
gRPC Ruby underscore rule (`GetHTTP` becomes `get_http`). Leading-underscore
|
|
131
|
+
RPCs, object hooks, implicit conversion methods, reserved runtime names and
|
|
132
|
+
collisions visible in the request fail with diagnostics. Compatible `google.*`
|
|
133
|
+
and `Google::...` Ruby package nesting is supported, including WKT imports;
|
|
134
|
+
replacing `Google`, `ConnectRPC` or concrete protobuf runtime constants is rejected.
|
|
135
|
+
Cross-invocation namespace conflicts cannot all be discovered; generated code
|
|
136
|
+
also guards its directly defined constants at load time. See [design](docs/design.md).
|
|
137
|
+
|
|
138
|
+
## Use unary GET
|
|
139
|
+
|
|
140
|
+
Declare safe queries with the standard protobuf method option:
|
|
141
|
+
|
|
142
|
+
```proto
|
|
143
|
+
rpc Read(Request) returns (Reply) {
|
|
144
|
+
option idempotency_level = NO_SIDE_EFFECTS;
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The generator maps `NO_SIDE_EFFECTS` to `idempotency: :no_side_effects`,
|
|
149
|
+
`IDEMPOTENT` to `:idempotent`, and an absent/unknown level to `:unknown`.
|
|
150
|
+
Manual `Method.new` declarations accept the same three symbols.
|
|
151
|
+
Enable GET on the core client; generated wrappers keep their existing API:
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
core = ConnectRPC::Client.new(base_url: "https://api.example.com", binding: binding,
|
|
155
|
+
use_get: true, get_url_limit: 8192)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Only `:no_side_effects` methods select GET. The default remains POST.
|
|
159
|
+
The URL cap counts the complete absolute URI in bytes, including the base
|
|
160
|
+
prefix and escaped query. Excess raises `resource_exhausted` before dispatch;
|
|
161
|
+
there is no POST fallback, retry or replay. Binary and gzip payloads use
|
|
162
|
+
unpadded URL-safe base64; identity JSON uses UTF-8 form encoding.
|
|
163
|
+
|
|
164
|
+
Servers admit GET automatically for declared safe methods. Native hosts and
|
|
165
|
+
Testing forward the raw query; direct `Server.call` callers pass `query: "..."`
|
|
166
|
+
separately from `path:`. `Server.new(get_query_limit: nil)` derives its raw-query
|
|
167
|
+
cap as `3 * (4 * ((request_wire_limit + 2) / 3)) + 1024` after validating the
|
|
168
|
+
wire limit. An explicit cap must be a positive integer. Serialized/decompressed
|
|
169
|
+
message bytes, decoded query payload bytes before inflation, and raw query/URL
|
|
170
|
+
bytes have independent limits. Hosts have already allocated the incoming URL;
|
|
171
|
+
these caps are not a total memory guarantee.
|
|
172
|
+
|
|
173
|
+
GET URLs can appear in proxy/server logs, browser history and generic HTTP
|
|
174
|
+
instrumentation. Choose safe request contents and application cache/security
|
|
175
|
+
policy accordingly. All GET responses vary on `Accept-Encoding`, preserving
|
|
176
|
+
application Vary tokens and `*`. The library never adds public Cache-Control.
|
|
177
|
+
Authentication, cookies, sessions and CSRF remain application/framework policy.
|
|
178
|
+
Official Ruby protobuf serialization is repeatable for the same exercised
|
|
179
|
+
object within a process, including gzip. It has no deterministic flag or
|
|
180
|
+
cross-process canonical cache-key guarantee, including for maps.
|
|
181
|
+
|
|
182
|
+
## Declare a unary method
|
|
183
|
+
|
|
184
|
+
Core `require "connectrpc"` needs no Google or Rack gems. Load optional
|
|
185
|
+
integrations explicitly. Development uses exact pins; the integration entry
|
|
186
|
+
points support google-protobuf `>= 4.36.2, < 5` and Rack `>= 3.2.7, < 4`.
|
|
187
|
+
Applications using Google must supply `google-protobuf`. Rack middleware ships
|
|
188
|
+
in the separate `connectrpc-rack` gem, which installs compatible core and Rack
|
|
189
|
+
dependencies. Existing middleware users must add this Gemfile dependency:
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
gem "connectrpc-rack", "~> 0.1.0"
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Keep `require "connectrpc/rack"` and the middleware API unchanged. Core installed
|
|
196
|
+
alone has no middleware file, so requiring that path raises an ordinary
|
|
197
|
+
`LoadError`. This is an installation migration; Rack already loaded only on
|
|
198
|
+
explicit require.
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
201
|
+
require "connectrpc/bindings/google_protobuf"
|
|
202
|
+
require "connectrpc/rack"
|
|
203
|
+
require "google/protobuf/wrappers_pb"
|
|
204
|
+
|
|
205
|
+
type = Google::Protobuf::StringValue
|
|
206
|
+
method = ConnectRPC::Method.new(
|
|
207
|
+
service: "example.v1.EchoService", name: "Echo",
|
|
208
|
+
request_type: type, response_type: type
|
|
209
|
+
)
|
|
210
|
+
service = ConnectRPC::Service.new(name: method.service, methods: [method])
|
|
211
|
+
binding = ConnectRPC::Bindings::GoogleProtobuf.new
|
|
212
|
+
handler = lambda do |request, context|
|
|
213
|
+
context.check!
|
|
214
|
+
context.response_metadata.add("x-worker", "ruby")
|
|
215
|
+
context.response_trailers.add("x-finished", "yes")
|
|
216
|
+
type.new(value: "Hello, #{request.value}!")
|
|
217
|
+
end
|
|
218
|
+
server = ConnectRPC::Server.new(
|
|
219
|
+
services: [service], binding: binding, handlers: { method.path => handler }
|
|
220
|
+
)
|
|
221
|
+
app = ConnectRPC::Rack.new(server: server) # Supply this to your Rack host.
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`ConnectRPC::Rack.new(downstream_app, server: server)` falls through for unrelated
|
|
225
|
+
services. An unknown method within a registered service returns `unimplemented`.
|
|
226
|
+
Rack routes `PATH_INFO`, so mounting under a prefix works. Handlers receive a
|
|
227
|
+
typed request and a fresh `Context` for each call. A custom
|
|
228
|
+
`invoker: ->(method, request, context) { ... }` can own execution instead.
|
|
229
|
+
Reusable handlers must synchronize any application state they share.
|
|
230
|
+
|
|
231
|
+
With that app hosted at a base URL, a client call is:
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
client = ConnectRPC::Client.new(
|
|
235
|
+
base_url: "http://127.0.0.1:8080", binding: binding, codec: :json
|
|
236
|
+
)
|
|
237
|
+
response = client.call(
|
|
238
|
+
method, type.new(value: "Ruby"), metadata: { "x-request" => ["example"] }, timeout: 2
|
|
239
|
+
)
|
|
240
|
+
puts response.message.value
|
|
241
|
+
p response.metadata.to_h
|
|
242
|
+
p response.trailers.to_h
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
The default codec is `:proto`; `:json` uses the native protobuf JSON mapping,
|
|
246
|
+
including int64 strings, enum names, presence, and well-known types. Decoding
|
|
247
|
+
ignores unknown JSON fields. The Google binding handles scalar wrapper roots
|
|
248
|
+
through the native Any decoder because protobuf 4.36 cannot parse those roots
|
|
249
|
+
directly. This does not change Connect error details: each `ErrorDetail` has a
|
|
250
|
+
protobuf type name and binary bytes, with a base64 representation on the wire.
|
|
251
|
+
|
|
252
|
+
Bindings implement `encode(message, type:, codec:)` and
|
|
253
|
+
`decode(bytes, type:, codec:)`, raising `ConnectRPC::CodecError` for codec
|
|
254
|
+
failures. Transports implement `call(TransportRequest)` and return a
|
|
255
|
+
`TransportResponse`. Their fields and responsibilities are documented in
|
|
256
|
+
[design](docs/design.md). Neither seam requires Google or Rack types.
|
|
257
|
+
|
|
258
|
+
## Unary gzip
|
|
259
|
+
|
|
260
|
+
Identity is the default. Configure `encoding: :gzip` on the client to compress
|
|
261
|
+
requests and advertise `gzip, identity` for responses. Configure it on the server
|
|
262
|
+
to accept gzip requests and negotiate gzip success responses in both codecs:
|
|
263
|
+
|
|
264
|
+
```ruby
|
|
265
|
+
client = ConnectRPC::Client.new(base_url: "http://127.0.0.1:8080", binding: binding,
|
|
266
|
+
encoding: :gzip)
|
|
267
|
+
server = ConnectRPC::Server.new(services: [service], binding: binding,
|
|
268
|
+
handlers: { method.path => handler }, encoding: :gzip)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Client `accept_encoding:` independently controls accepted responses. Its default
|
|
272
|
+
is derived from request encoding. An explicit nonempty array contains distinct
|
|
273
|
+
`:gzip`/`:identity` symbols; identity is always appended as the last fallback.
|
|
274
|
+
For example, `accept_encoding: [:gzip]` permits identity requests with gzip
|
|
275
|
+
responses; `encoding: :gzip, accept_encoding: [:identity]` permits gzip requests
|
|
276
|
+
with identity responses. The server selects a supported compression coding from
|
|
277
|
+
the ordered accept fields before identity. An absent field inherits the request
|
|
278
|
+
coding; an explicitly empty field selects identity. Unknown tokens and quality
|
|
279
|
+
values are ignored. Only one identity or gzip content coding is accepted.
|
|
280
|
+
|
|
281
|
+
Empty serialized messages selected for gzip produce valid nonempty gzip members.
|
|
282
|
+
Zero-length HTTP content is never inflated, after its coding header is validated.
|
|
283
|
+
Concatenated members are supported under one aggregate decoded limit; every
|
|
284
|
+
member's CRC, size and completion are validated. Truncation and trailing garbage
|
|
285
|
+
are rejected. Server errors remain identity; clients can decode compressed peer
|
|
286
|
+
errors, including typed details and metadata. Malformed compressed HTTP errors
|
|
287
|
+
use HTTP fallback, while malformed compressed success responses return internal.
|
|
288
|
+
|
|
289
|
+
## Metadata and failures
|
|
290
|
+
|
|
291
|
+
Metadata keys are normalized to lower case and values are arrays of printable
|
|
292
|
+
ASCII header strings. Protocol and framing keys are reserved. Use
|
|
293
|
+
`metadata.add_binary("key-bin", bytes)` and `metadata.binary("key-bin")` for
|
|
294
|
+
binary values. Writers emit unpadded base64; readers accept valid padded and
|
|
295
|
+
unpadded base64. Only binary helpers split coalesced comma-separated values.
|
|
296
|
+
Other metadata values keep their received boundaries.
|
|
297
|
+
Incoming HTTP fields whose names or values cannot be represented as Connect
|
|
298
|
+
metadata are omitted, including the entire field if any repeated value is
|
|
299
|
+
invalid. Valid adjacent fields remain available in leading and trailing
|
|
300
|
+
metadata. Outbound names and values are always validated strictly.
|
|
301
|
+
|
|
302
|
+
Rack responses preserve arrays. Net::HTTP preserves repeated raw response
|
|
303
|
+
fields, but joins outgoing request arrays with `", "`. rackup/WEBrick also
|
|
304
|
+
coalesces incoming and response fields. For example, `['a', 'b']` may arrive as
|
|
305
|
+
`['a, b']`. Interop asserts these observed values; original outbound field
|
|
306
|
+
boundaries are not preserved.
|
|
307
|
+
|
|
308
|
+
Raise a deliberate `ConnectRPC::Error.new(:invalid_argument, "safe message",
|
|
309
|
+
details: [...], metadata: {...}, trailers: {...})` from a handler. Unexpected
|
|
310
|
+
application exceptions become `internal` with `"Internal error"`. Clients raise
|
|
311
|
+
`ConnectRPC::Error` with `code`, `details`, `metadata`, and `trailers`. Decode a
|
|
312
|
+
detail with `detail.decode(binding: binding, type: message_class)` after choosing
|
|
313
|
+
the class corresponding to `detail.type`.
|
|
314
|
+
|
|
315
|
+
## Limits and deadlines
|
|
316
|
+
|
|
317
|
+
Serialized/decompressed request and response limits default to 4 MiB each.
|
|
318
|
+
Set `request_limit:` and `response_limit:` on either endpoint to positive integers.
|
|
319
|
+
Separate `request_wire_limit:` and `response_wire_limit:` cap encoded HTTP bytes
|
|
320
|
+
and default to their corresponding message limits. Identity must satisfy both
|
|
321
|
+
caps. Gzip can expand small messages, including empty-message framing; explicitly
|
|
322
|
+
increase the wire cap if that expansion is desired. Compression never silently
|
|
323
|
+
falls back to identity after exceeding a cap. `TransportRequest.response_limit`
|
|
324
|
+
remains the wire read cap for custom transports.
|
|
325
|
+
|
|
326
|
+
Input is bounded before inflation and binding decode, including unknown-length
|
|
327
|
+
Rack input and HTTP errors. Native gzip output is checked before each append,
|
|
328
|
+
with at most one 16 KiB output callback beyond the retained limit. This is not a
|
|
329
|
+
total allocation bound: wire and decoded buffers coexist, and native serialization
|
|
330
|
+
can allocate the whole message before its size check. Server identity errors use
|
|
331
|
+
the smaller response cap and safe replacement; if the replacement cannot fit,
|
|
332
|
+
they send an empty 429 and clients apply HTTP fallback. Header size limits and
|
|
333
|
+
HTTP input coalescing belong to the library and host.
|
|
334
|
+
|
|
335
|
+
Client `timeout:` accepts finite positive seconds, defaulting to 30. Explicit
|
|
336
|
+
`timeout: nil` sends no timeout header and creates no logical deadline or expiry
|
|
337
|
+
watchdog. Native TCP/TLS/read/write waits remain independently capped at
|
|
338
|
+
`9_999_999.999` seconds; a native timeout returns `unavailable`. Sustained progress
|
|
339
|
+
can outlast that per-operation cap. For finite values, one monotonic
|
|
340
|
+
budget starts before encoding and covers network I/O and checks around codec
|
|
341
|
+
work. NetHTTP uses one connection per call, closes it on every outcome, sends
|
|
342
|
+
the configured `Accept-Encoding`, ignores proxy environment variables, disables
|
|
343
|
+
retries and decompression, preserves TLS peer/hostname verification, and does
|
|
344
|
+
not follow redirects. A transport-owned watchdog closes its socket at expiry,
|
|
345
|
+
including during response-header trickle and blocked writes. Native DNS
|
|
346
|
+
resolution may outlast the budget; expiry is checked when control returns.
|
|
347
|
+
Codec work and request preparation are synchronous and checked before/after;
|
|
348
|
+
gzip work also checks each native input/output chunk and member boundary. They are
|
|
349
|
+
not interrupted. Dispatch rechecks expiry and refreshes the wire timeout after
|
|
350
|
+
connection setup; a closed socket cannot trigger an implicit reconnect. Native
|
|
351
|
+
operations are independently capped at `9_999_999.999` seconds, the Connect
|
|
352
|
+
wire maximum, to avoid platform overflow for very large budgets. A native
|
|
353
|
+
operation timeout before logical expiry returns `unavailable`; actual expiry
|
|
354
|
+
returns `deadline_exceeded`. The logical deadline stays unchanged, and the
|
|
355
|
+
watchdog rechecks it after each capped wait.
|
|
356
|
+
|
|
357
|
+
Explicit unary cancellation uses a permanent, shareable token:
|
|
358
|
+
|
|
359
|
+
```ruby
|
|
360
|
+
source = ConnectRPC::CancellationSource.new
|
|
361
|
+
# Pass source.token to each call that should share cancellation.
|
|
362
|
+
response = client.call(method, message, timeout: nil, cancellation: source.token)
|
|
363
|
+
# Another thread or reactor task can call source.cancel while the call blocks.
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
`source.cancel` returns true only for its first transition. A token exposes
|
|
367
|
+
`cancelled?`, `reason` (`:canceled` or nil), `check!`, and
|
|
368
|
+
`on_cancel { ... }`. Registration returns an object with `unregister`, which
|
|
369
|
+
returns true only when it prevents an invocation that has not started. It never
|
|
370
|
+
waits for a running listener. Listeners run in the cancelling thread, must be
|
|
371
|
+
short and nonblocking, and isolate `StandardError`. Unregistering releases the
|
|
372
|
+
callback; completing a call releases its internal registrations. Sources cannot
|
|
373
|
+
be reset, and a call never completes or changes another call's shared token.
|
|
374
|
+
|
|
375
|
+
Each call accepts one terminal outcome. An already-expired deadline takes
|
|
376
|
+
precedence while the call is open, followed by observed explicit cancellation.
|
|
377
|
+
Accepted cancellation stays canceled through later expiry. A committed response
|
|
378
|
+
or error stays final after later source cancellation. Source transition time is
|
|
379
|
+
not acceptance time: a slow earlier listener may delay acceptance until expiry.
|
|
380
|
+
Final decoding/error commit checks the source directly, even if notification is
|
|
381
|
+
delayed. Cancellation never substitutes a timeout header for `timeout: nil`.
|
|
382
|
+
|
|
383
|
+
NetHTTP closes only its call-owned socket to interrupt exposed TLS, writes,
|
|
384
|
+
headers and body reads. Native DNS/TCP before socket exposure remains
|
|
385
|
+
cooperative; cancellation is checked on return before any later dispatch.
|
|
386
|
+
The optional Async transport wakes its owner reactor and resets its stream.
|
|
387
|
+
Healthy multiplexed siblings survive; a stuck frame retains the documented
|
|
388
|
+
connection-wide cleanup fallback. Neither backend retries or replays a call.
|
|
389
|
+
|
|
390
|
+
Server contexts expose `deadline`, `cancelled?`, `cancellation_reason`
|
|
391
|
+
(`:canceled`, `:deadline_exceeded`, or nil), and `check!`. Handlers cooperate at
|
|
392
|
+
application boundaries. Low-level `Server#call(cancellation:)` and the Testing
|
|
393
|
+
transport accept explicit cancellation. Native HTTP/2 publishes peer
|
|
394
|
+
`RST_STREAM(CANCEL)` before input teardown; normal END_STREAM/half-close, other
|
|
395
|
+
reset codes, connection loss and host shutdown do not manufacture explicit
|
|
396
|
+
cancellation. Rack/Rails and Falcon HTTP/1 add no peer detection or token env
|
|
397
|
+
injection. Synchronous adapters cannot interrupt blocked application code.
|
|
398
|
+
See [design](docs/design.md) for ownership and transport limits.
|
|
399
|
+
|
|
400
|
+
CRuby >= 3.4 is the baseline; project verification targets Ruby 3.4.12 and
|
|
401
|
+
4.0.9 on Linux x64. Local setup builds from source; CI uses official prebuilt
|
|
402
|
+
runtimes. See [development](docs/development.md) for the
|
|
403
|
+
commands, dependency policy, isolated installation and evidence scope. The
|
|
404
|
+
interop fixture runs 18 cases: six Go-to-Go reference baseline cases and twelve
|
|
405
|
+
Ruby-to-Go/Go-to-Ruby cases. Both codecs share typed success, deliberate error
|
|
406
|
+
and request-limit scenarios. This covers the advertised unary subset, not full
|
|
407
|
+
conformance. A separate gzip portion adds 40 cases with actual wire observations,
|
|
408
|
+
including asymmetric raw Go HTTP probes and documented reference-client quirks.
|
|
409
|
+
|
|
410
|
+
The pinned [official conformance harness](conformance/README.md) independently
|
|
411
|
+
executes 96 client and 90 server identity cases, including two safe GET cases
|
|
412
|
+
per direction. The disjoint gzip profile adds 88 client and 82 server cases.
|
|
413
|
+
All 184 client and 172 server cases must pass with zero expected failures.
|
|
414
|
+
The four caller-cancellation cases use their original 5 ms asynchronous timer,
|
|
415
|
+
200 ms response request and no RPC deadline. The ledger requires actual request
|
|
416
|
+
writes, source cancellation, owned socket cleanup and the library's canceled
|
|
417
|
+
result. This is a bounded profile, not full Connect conformance.
|
|
418
|
+
|
|
419
|
+
## Status and licensing
|
|
420
|
+
|
|
421
|
+
Original project work is licensed under the [MIT License](LICENSE), copyright
|
|
422
|
+
2026 Jim Myhrberg. The core, Rack, Rails, Async and testing gem archives each include the
|
|
423
|
+
complete license. Third-party dependencies and imported fixtures retain their
|
|
424
|
+
own licenses and notices, including the separately licensed
|
|
425
|
+
[conformance schemas](conformance/README.md).
|
|
426
|
+
|
|
427
|
+
The gems pin `allowed_push_host` to `https://rubygems.org` and require MFA for
|
|
428
|
+
manual pushes. Releases are published only by the tag-triggered
|
|
429
|
+
[release workflow](docs/releasing.md), using RubyGems trusted publishing with
|
|
430
|
+
Sigstore attestations.
|
|
431
|
+
|
|
432
|
+
## Optional standalone HTTP/2
|
|
433
|
+
|
|
434
|
+
The [connectrpc-async package](gems/connectrpc-async/README.md) provides explicit
|
|
435
|
+
`connectrpc/async` integration: `ConnectRPC::Async::Host` and a matching pooled
|
|
436
|
+
`Transport` for Connect unary and streaming protobuf/ProtoJSON over h2c and verified TLS `h2`.
|
|
437
|
+
It supports required client authentication, complete live TLS snapshot reloads
|
|
438
|
+
on the same listener, older-generation drain, and an authenticated client leaf
|
|
439
|
+
accessor scoped to the exact handler task. Applications own the Async reactor,
|
|
440
|
+
authorization and lifecycle. There is no HTTP/1 fallback or automatic replay. See the package guide for defaults, bounded cleanup and backend
|
|
441
|
+
header normalization. `mise run async:example` runs the generated localhost
|
|
442
|
+
example; `async:verify` checks the independent bundle, installed packages and
|
|
443
|
+
connect-go interoperability. Core require and core/Rails dependencies stay
|
|
444
|
+
independent.
|
|
445
|
+
|
|
446
|
+
The same package provides `HTTP1Transport.new(origin:, connection_limit: 4,
|
|
447
|
+
max_requests: 128, connect_timeout: 10)` for explicit Connect HTTP/1.1 unary
|
|
448
|
+
(including safe GET), server streaming and client streaming. Pass it through the
|
|
449
|
+
existing core/generated client's `transport:` option inside an application-owned
|
|
450
|
+
Async reactor. TLS/mTLS verifies peer and hostname, offers only `http/1.1`, and
|
|
451
|
+
accepts absent ALPN for legacy H1 servers. There is no gRPC, bidi, replay or
|
|
452
|
+
protocol fallback. See the [package guide](gems/connectrpc-async/README.md#explicit-http11-client)
|
|
453
|
+
for lease capacity, deadlines and upload failure limits. Ordinary-thread
|
|
454
|
+
persistent clients remain [issue #29](https://github.com/jimeh/connectrpc-ruby/issues/29).
|
|
455
|
+
|
|
456
|
+
See the optional [in-process testing package](gems/connectrpc-testing/README.md) for real encoded client/server round trips without sockets.
|
|
457
|
+
|
|
458
|
+
## Optional Falcon hosting
|
|
459
|
+
|
|
460
|
+
The [connectrpc-falcon package](gems/connectrpc-falcon/README.md) serves the native
|
|
461
|
+
application in real Falcon process workers, with explicit Rack/Rails
|
|
462
|
+
fallback, h2/HTTP1 TLS selection and worker-local versioned TLS providers.
|
|
463
|
+
RPC compression and byte limits stay outside generic Falcon middleware.
|
|
464
|
+
WebSockets, ActionCable, upgrades, CONNECT and hijack are rejected on this
|
|
465
|
+
listener. `mise run falcon:example` exercises the normal service DSL;
|
|
466
|
+
`falcon:verify` checks its independent bundle, protocols, workers, packages and
|
|
467
|
+
pinned Go interoperability. Core, Rails and Async bundles remain independent.
|
|
468
|
+
|
|
469
|
+
## RPC lifecycle instrumentation
|
|
470
|
+
|
|
471
|
+
Pass `instrumentation: ConnectRPC::Instrumentation.new(subscribers: [recorder])`
|
|
472
|
+
to a Client or Server for synchronous per-call observation. A subscriber's
|
|
473
|
+
`start(call)` returns an observer with optional `attach`, `detach(token)`,
|
|
474
|
+
`processing_finished(outcome)` and `finished(completion)` methods. Core owns the
|
|
475
|
+
application yield and isolates each StandardError from subscribers and the
|
|
476
|
+
optional `on_error:` reporter. Original exceptions are process-local trusted
|
|
477
|
+
subscriber data. Instrumentation is disabled by default and performs no lifecycle
|
|
478
|
+
allocation or instrumentation clock reads on that path.
|
|
479
|
+
|
|
480
|
+
The optional [connectrpc-otel package](gems/connectrpc-otel/README.md) turns this
|
|
481
|
+
lifecycle into real Connect CLIENT/SERVER traces using an application-owned
|
|
482
|
+
OpenTelemetry SDK. Core has no OpenTelemetry dependency. See
|
|
483
|
+
[design](docs/design.md#rpc-lifecycle-observation) for host ownership and
|
|
484
|
+
`mise run otel:example` for a runnable generated caller with real SDK exports.
|
|
485
|
+
|
|
486
|
+
## Native Connect streaming
|
|
487
|
+
|
|
488
|
+
Async and Falcon native HTTP/2 support server, client and bidirectional streaming.
|
|
489
|
+
Falcon also serves Connect server and client streaming over HTTP/1.1 with both
|
|
490
|
+
codecs and identity/gzip over TCP/TLS/mTLS. Use the explicit
|
|
491
|
+
`ConnectRPC::Async::HTTP1Transport` for Ruby H1 unary, server streaming and
|
|
492
|
+
client streaming. The standalone Async Host stays H2-only. Early H1 responses finish
|
|
493
|
+
before bounded input discard. Retiring delivered responses use one cooperative
|
|
494
|
+
500 ms total final-close budget from the actual final HTTP flush, including the
|
|
495
|
+
unchanged 100 ms / 256 KiB discard allowance. Admission remains held until
|
|
496
|
+
retirement and original processing join.
|
|
497
|
+
See the [streaming guide](docs/streaming.md) for scoped APIs, mixed generated
|
|
498
|
+
services, bounds and the one-second HTTP tail completion policy.
|
|
499
|
+
|
|
500
|
+
Explicit server gRPC-Web support is documented in the [Web guide](docs/grpc-web.md),
|
|
501
|
+
including native H1/H2 streaming, portable unary, CORS and the optional official browser harness.
|