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
data/docs/design.md
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
# Design and implemented profile
|
|
2
|
+
|
|
3
|
+
The [supported 0.x contract](support.md) defines the target compatibility
|
|
4
|
+
boundary and the remaining adoption gates.
|
|
5
|
+
|
|
6
|
+
The gem implements typed Connect RPCs and native HTTP/2 gRPC in all four RPC shapes.
|
|
7
|
+
Falcon also serves Connect HTTP/1.1 server/client streaming. The explicit Async
|
|
8
|
+
HTTP1Transport supports Connect unary and server/client streaming; standalone
|
|
9
|
+
Async Host remains H2-only. The streaming guide records
|
|
10
|
+
the response-first discard limits and cooperative 500 ms total final-close
|
|
11
|
+
policy. Shared protocol code is independent of HTTP hosting and concrete
|
|
12
|
+
protobuf classes. The public require is `connectrpc`, and the Ruby namespace is
|
|
13
|
+
`ConnectRPC`. The API is experimental.
|
|
14
|
+
|
|
15
|
+
## Shared boundaries
|
|
16
|
+
|
|
17
|
+
`Method` holds validated service/method names, request/response binding types,
|
|
18
|
+
`cardinality: :unary`, `idempotency: :unknown` (or `:no_side_effects` / `:idempotent`),
|
|
19
|
+
and a case-sensitive path. Only `:no_side_effects` admits GET. `Service` validates its method
|
|
20
|
+
set. Streaming uses invariant sibling declarations, keeping unary Method
|
|
21
|
+
assignability intact. Generated service glue reuses these objects.
|
|
22
|
+
The [streaming guide](streaming.md) owns its scoped APIs, publication/completion
|
|
23
|
+
boundaries, finite transport SPI and absolute recovery/tail policies.
|
|
24
|
+
|
|
25
|
+
`Client` owns request encoding, protocol headers, response validation, errors,
|
|
26
|
+
metadata, byte limits and the deadline. Base URL path prefixes are retained.
|
|
27
|
+
A binding provides `encode(message, type:, codec:)` and
|
|
28
|
+
`decode(bytes, type:, codec:)`. Custom bindings raise `CodecError` for invalid
|
|
29
|
+
messages. The optional Google binding delegates binary and
|
|
30
|
+
[protobuf JSON](https://protobuf.dev/programming-guides/json/) to native
|
|
31
|
+
protobuf APIs. It ignores unknown JSON fields. Scalar wrapper roots use a
|
|
32
|
+
standard Any envelope internally to work around protobuf 4.36's direct-root
|
|
33
|
+
decoder limitation; the backend still validates the protobuf values.
|
|
34
|
+
|
|
35
|
+
A transport provides `call(request)` with these host-neutral value objects:
|
|
36
|
+
|
|
37
|
+
| Object | Fields |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| `TransportRequest` | `url` (URI), `http_method`, `headers` (lower-case keys, string arrays), `body` (bytes), `response_limit`, `deadline`, `lifecycle`, `cancellation` |
|
|
40
|
+
| `TransportResponse` | `status` (integer), `headers` (repeated string arrays), `body` (bounded bytes) |
|
|
41
|
+
| `Response` | `message` (binding type), `metadata`, `trailers` (Metadata objects) |
|
|
42
|
+
|
|
43
|
+
`TransportRequest.deadline` is a `Deadline` or `nil`. Custom transports must
|
|
44
|
+
enforce a supplied deadline and read-only `cancellation` SPI during their owned
|
|
45
|
+
I/O and bound the body incrementally.
|
|
46
|
+
An absent deadline means no aggregate RPC expiry or wire timeout. Client validation checks their output again but
|
|
47
|
+
cannot undo an oversized allocation or interrupt their work.
|
|
48
|
+
|
|
49
|
+
`Server` owns negotiation, bounded input, decoding, invocation, serialization,
|
|
50
|
+
and safe errors. `Rack` only translates request/response HTTP state. It wraps
|
|
51
|
+
an optional downstream app and routes `PATH_INFO`; unrelated service paths
|
|
52
|
+
fall through, while unknown methods within registered services fail explicitly.
|
|
53
|
+
Neither core nor handlers need Rack envs. The optional `connectrpc-rack` package
|
|
54
|
+
owns `connectrpc/rack` exclusively and loads core through `require "connectrpc"`.
|
|
55
|
+
Applications add `gem "connectrpc-rack", "~> 0.1.0"` while keeping the require
|
|
56
|
+
and `ConnectRPC::Rack` API unchanged. Core has no middleware file or runtime
|
|
57
|
+
dependencies; requiring middleware without its package raises an ordinary
|
|
58
|
+
`LoadError`. Google loads only through `connectrpc/bindings/google_protobuf`;
|
|
59
|
+
its missing/incompatible dependency diagnostic remains useful. The middleware
|
|
60
|
+
retains its own Rack version activation diagnostic.
|
|
61
|
+
|
|
62
|
+
A default invoker calls the registered plain handler with `(request, context)`.
|
|
63
|
+
An explicit `invoker` callable receives `(method, request, context)` and can
|
|
64
|
+
supply framework execution later. Every invocation has a fresh `Context` with
|
|
65
|
+
request metadata, response leading/trailing metadata, and optional deadline.
|
|
66
|
+
Handlers are reusable; application-owned shared state needs its own locking.
|
|
67
|
+
|
|
68
|
+
## Wire behavior
|
|
69
|
+
|
|
70
|
+
The [native gRPC guide](grpc.md) describes protobuf-only native HTTP/2 execution,
|
|
71
|
+
protocol configuration and status/trailer policies. The behavior below describes
|
|
72
|
+
the default Connect profile; portable adapters support Connect and explicitly enabled gRPC-Web unary.
|
|
73
|
+
The [gRPC-Web guide](grpc-web.md) owns its independent media, CORS, bounds and terminal rules.
|
|
74
|
+
|
|
75
|
+
The implemented [Connect profile](https://connectrpc.com/docs/protocol/) is
|
|
76
|
+
unary POST and safe unary GET, identity or opt-in gzip encoding, and `application/proto` or `application/json`.
|
|
77
|
+
JSON also accepts `charset=utf-8`. Clients send protocol version `1`; servers
|
|
78
|
+
accept an absent version and reject any other supplied value. A 200 response
|
|
79
|
+
must match the request codec. Non-application response types produce `unknown`;
|
|
80
|
+
mismatched application codecs and unsupported/unadvertised response encodings produce
|
|
81
|
+
`internal`. JSON errors are only parsed for an appropriate JSON content type.
|
|
82
|
+
Single identity/gzip content codings are case-insensitive; empty, repeated,
|
|
83
|
+
stacked and unknown content codings are rejected. Identity-only servers continue
|
|
84
|
+
to reject gzip. Client response preferences use distinct symbol arrays and
|
|
85
|
+
normalize identity to the last fallback. Server gzip capability negotiates
|
|
86
|
+
supported nonidentity accept tokens before identity, ignoring unknown/quality
|
|
87
|
+
value tokens. Absent accept inherits request coding; empty accept selects identity.
|
|
88
|
+
Repeated accept fields combine in field order. Gzip-selected empty serialization
|
|
89
|
+
produces a valid nonempty member; zero-length HTTP content bypasses inflation
|
|
90
|
+
after header validation. Every concatenated member validates CRC/ISIZE/completion;
|
|
91
|
+
truncation and trailing data fail under an aggregate decoded cap.
|
|
92
|
+
|
|
93
|
+
Other verbs get bare 405 with `Allow: GET, POST` for safe methods, otherwise `POST`; unsupported types, including
|
|
94
|
+
Connect streaming and gRPC variants on portable adapters, get bare 415 with `Accept-Post`.
|
|
95
|
+
Unsupported request encoding returns `unimplemented` listing supported codings. Invalid
|
|
96
|
+
version, timeout, or request decoding returns `invalid_argument`. Limits return
|
|
97
|
+
`resource_exhausted`. Unexpected application exceptions return generic
|
|
98
|
+
`internal`; deliberate `Error` messages are application-controlled safe text.
|
|
99
|
+
Standard Connect-to-HTTP and fallback HTTP-to-Connect tables are used. Null, absent and unrecognized wire codes use the HTTP fallback while preserving
|
|
100
|
+
valid message and details. Malformed bodies, code types or details use the
|
|
101
|
+
HTTP fallback without decoding those error fields. Error details preserve a type name and binary bytes, with
|
|
102
|
+
unpadded base64 on output and validated padded/unpadded input.
|
|
103
|
+
|
|
104
|
+
`Metadata` stores lower-case keys and repeated printable ASCII values and
|
|
105
|
+
rejects protocol/framing overrides and header injection. Binary `-bin` helpers
|
|
106
|
+
handle base64 and comma coalescing. Trailing metadata becomes `trailer-`
|
|
107
|
+
response fields. Other fields are never blindly comma-split. The middleware
|
|
108
|
+
emits separate arrays under `Rack::Lint`, and NetHTTP preserves raw repeated
|
|
109
|
+
response fields. NetHTTP joins outgoing request arrays with comma+space.
|
|
110
|
+
rackup/WEBrick additionally coalesces incoming and response fields; interop
|
|
111
|
+
asserts those observed values explicitly. Received fields outside the metadata
|
|
112
|
+
name/printable-ASCII contract are omitted during extraction, including an
|
|
113
|
+
entire field with any invalid repeated value. Valid adjacent leading/trailing
|
|
114
|
+
fields survive. Protocol validation is separate, and outbound metadata remains
|
|
115
|
+
strictly validated.
|
|
116
|
+
|
|
117
|
+
GET uses the existing codec, compression, deadline and metadata paths. The
|
|
118
|
+
client opts in with `use_get: true`; `get_url_limit: 8192` bounds the complete
|
|
119
|
+
absolute URI before transport dispatch, without fallback. Canonical controls
|
|
120
|
+
are `connect=v1`, optional `base64=1`, optional `compression=gzip`, `encoding`,
|
|
121
|
+
and `message`. Binary/gzip use unpadded URL-safe base64; JSON identity uses
|
|
122
|
+
UTF-8 form encoding. Input accepts padded URL-safe base64, arbitrary parameter
|
|
123
|
+
order, unknown/repeated unknown fields and binary unknown values. Form decoding
|
|
124
|
+
happens once: `+` is space, `%2B` is plus, and `;` is literal data.
|
|
125
|
+
|
|
126
|
+
The bounded parser validates escapes everywhere and rejects duplicate known
|
|
127
|
+
names after name decoding as a local ambiguity policy. Only text message values
|
|
128
|
+
require UTF-8. Query syntax errors can precede codec rejection. Missing or
|
|
129
|
+
unsupported query encoding gets bare 415. Content-Length syntax follows, then
|
|
130
|
+
positive declared/actual content gets bare 415 after at most one requested
|
|
131
|
+
byte. Query compression, version, message presence and deferred timeout errors
|
|
132
|
+
follow in that order. Missing/empty compression means identity; missing/empty
|
|
133
|
+
connect version is accepted. Other supplied versions must be `v1`. Query
|
|
134
|
+
controls supersede irrelevant POST protocol headers. Valid deadlines start
|
|
135
|
+
before parsing and are checked cooperatively throughout processing.
|
|
136
|
+
|
|
137
|
+
Unsafe GET gets bare 405 before query/header/input work. An unknown owned
|
|
138
|
+
suffix can negotiate GET and then return `unimplemented`; it never invokes a
|
|
139
|
+
handler or supplies a fabricated safe Method to observation. Every GET response,
|
|
140
|
+
including bare errors, merges Vary after application/error metadata, preserving
|
|
141
|
+
case-insensitive tokens and wildcard `*`. Cache-Control belongs to the app.
|
|
142
|
+
Native NetHTTP/Async requests have no body or synthetic content headers.
|
|
143
|
+
|
|
144
|
+
`Server.call(query: "")` keeps path and raw query separate. The raw-query default
|
|
145
|
+
is `3 * (4 * ((validated_request_wire_limit + 2) / 3)) + 1024`, including fully
|
|
146
|
+
percent-escaped padded base64 and bounded parameter overhead. It is independent
|
|
147
|
+
of decoded query payload and inflated message caps. Parsing retains only five
|
|
148
|
+
controls, validates ignored values without storing them, and checks expansion
|
|
149
|
+
before appending. Incoming URLs and native serialization already exist before
|
|
150
|
+
these checks; no total allocation bound is claimed.
|
|
151
|
+
|
|
152
|
+
GET request byte observation measures the percent/base64-decoded transport
|
|
153
|
+
payload before inflation. Unknown measurements stay nil; only measured empty
|
|
154
|
+
payloads are zero. No URL, query or payload enters RPC observation. Host/proxy
|
|
155
|
+
logs and generic HTTP tracing remain application-owned. Ruby's official native
|
|
156
|
+
protobuf encoder has no deterministic flag; same-object within-process encoding
|
|
157
|
+
and gzip are tested, but map encodings vary across processes. No semantic or
|
|
158
|
+
cross-process canonical cache-key promise is made.
|
|
159
|
+
|
|
160
|
+
## Bounds and lifecycle
|
|
161
|
+
|
|
162
|
+
Default serialized/decompressed request/response limits are 4 MiB each.
|
|
163
|
+
Independent wire limits default to the corresponding message limits, preserving
|
|
164
|
+
both bounds for existing callers. `TransportRequest.response_limit` is the
|
|
165
|
+
encoded response read cap. Declared lengths are validated against wire bytes and
|
|
166
|
+
oversized lengths rejected before reading. Unknown-length input stops at the
|
|
167
|
+
first excess byte. Identity satisfies both caps. Outgoing serialization is checked
|
|
168
|
+
before compression; compressed output, including framing/footer, must fit its
|
|
169
|
+
wire cap without fallback. Explicitly larger wire limits permit small-message
|
|
170
|
+
expansion without increasing message bounds.
|
|
171
|
+
|
|
172
|
+
Gzip uses default Zlib gzip-only streams with bounded input and block output,
|
|
173
|
+
checking size before every append. At most one 16 KiB native output callback can
|
|
174
|
+
exceed retained payload, plus native state. Every member/chunk checks cooperative
|
|
175
|
+
expiry; streams close on every outcome. Wire and decoded buffers coexist, and
|
|
176
|
+
native protobuf/JSON serialization can allocate before its output check. Custom
|
|
177
|
+
transports may allocate before returning; core revalidation cannot undo that.
|
|
178
|
+
There is no total allocation, hard CPU or independent header-size guarantee.
|
|
179
|
+
|
|
180
|
+
Server errors remain identity and use the smaller response cap, including the
|
|
181
|
+
safe replacement and empty-429 fallback. Valid gzip peer errors are decoded
|
|
182
|
+
before JSON parsing. Malformed gzip or JSON on non-200 uses HTTP fallback with
|
|
183
|
+
valid metadata/trailers; malformed gzip 200 is internal. Header rejection stays
|
|
184
|
+
internal, local size excess stays resource_exhausted, and actual expiry stays
|
|
185
|
+
deadline_exceeded. No generic error-body rescue changes those classifications.
|
|
186
|
+
|
|
187
|
+
The default timeout is 30 seconds. Explicit `timeout: nil` means no logical
|
|
188
|
+
client deadline, timeout header or expiry watchdog. Native operation waits still
|
|
189
|
+
use the exact `9_999_999.999` second cap, returning `unavailable` on timeout.
|
|
190
|
+
Sustained progress can outlast this per-operation cap. For finite positive
|
|
191
|
+
timeouts, one monotonic client deadline begins before encoding. Remaining timeout milliseconds are
|
|
192
|
+
rounded up and clamped to ten digits without extending actual expiry. NetHTTP
|
|
193
|
+
recomputes the header at dispatch and checks the budget around connection,
|
|
194
|
+
codec and body progress. Each call owns one connection; a finite deadline also owns one watchdog. The
|
|
195
|
+
watchdog closes the owned raw socket on expiry, including during header
|
|
196
|
+
trickle, stalled reads and blocked writes, and is joined on every exit.
|
|
197
|
+
NetHTTP depends on the pinned stdlib BufferedIO socket layout for that close.
|
|
198
|
+
Request preparation finishes before connecting, with an expiry check afterward.
|
|
199
|
+
The connection checks expiry and recomputes the wire timeout immediately before
|
|
200
|
+
dispatch, and rejects Net::HTTP's implicit reconnect of a closed socket. This
|
|
201
|
+
keeps every dispatched byte on the one socket owned by the watchdog.
|
|
202
|
+
TCP/TLS/read/write timeout arguments and watchdog waits are capped at exactly
|
|
203
|
+
`9_999_999.999` seconds, the Connect wire maximum, for safe native time conversion.
|
|
204
|
+
Individual native operations are independently bounded: an operation timeout
|
|
205
|
+
before logical expiry returns `unavailable`, while actual expiry returns
|
|
206
|
+
`deadline_exceeded`. Logical expiry remains unchanged; the watchdog re-evaluates
|
|
207
|
+
it after each capped wait and only closes the socket at actual expiry. Local
|
|
208
|
+
huge-budget calls verify conversions on the exercised plain TCP paths, not an
|
|
209
|
+
actual 115-day duration or runtime TLS behavior.
|
|
210
|
+
|
|
211
|
+
DNS/native resolution may outlast the budget. Net::HTTP's internal connection
|
|
212
|
+
implementation can itself use `Timeout.timeout` on Ruby 3.4; ConnectRPC never
|
|
213
|
+
calls it or asynchronously interrupts application code. TLS handshake timeout
|
|
214
|
+
uses the remaining budget after TCP/DNS. Peer/hostname verification stays on.
|
|
215
|
+
There is no pooling, proxy-env routing, retries, redirect following, automatic
|
|
216
|
+
decompression. NetHTTP constructs requests with the actual explicit accept
|
|
217
|
+
advertisement so the default HTTP gem never transparently inflates or rewrites
|
|
218
|
+
content encoding/length; transports carry opaque encoded bytes.
|
|
219
|
+
|
|
220
|
+
Server timeout validation creates a per-call monotonic deadline. Checks run
|
|
221
|
+
around input, decoding, handler execution and encoding. `cancelled?` combines explicit cancellation and expiry;
|
|
222
|
+
`cancellation_reason` identifies `:canceled` or `:deadline_exceeded`, and handlers
|
|
223
|
+
can call `check!` cooperatively. No header means no server
|
|
224
|
+
expiry. Synchronous Rack cannot interrupt blocked input, arbitrary handlers or
|
|
225
|
+
codec work, or detect every disconnect. No hard server cancellation is claimed.
|
|
226
|
+
|
|
227
|
+
## Explicit cancellation ownership
|
|
228
|
+
|
|
229
|
+
`CancellationSource` alone initiates permanent cancellation. Its read-only
|
|
230
|
+
`CancellationToken` can be shared across calls. `cancel` returns true only on
|
|
231
|
+
its first transition. Registration and invocation claims are synchronized;
|
|
232
|
+
callbacks run outside locks in the cancelling thread. Already-cancelled
|
|
233
|
+
registration invokes before returning. `unregister` returns true only for a
|
|
234
|
+
pending registration, suppresses even snapshotted pending callbacks and releases
|
|
235
|
+
references. Running/finished/unregistered listeners return false without waiting.
|
|
236
|
+
Listeners must be short and nonblocking; `StandardError` does not stop siblings.
|
|
237
|
+
Internal resource retirement separately guards callbacks that already started.
|
|
238
|
+
|
|
239
|
+
Client and Context each own distinct terminal arbitration. While open, they
|
|
240
|
+
accept an already-expired real deadline first, then directly observed source
|
|
241
|
+
cancellation, then a result/error. Accepted reasons never flip. A source's
|
|
242
|
+
transition requests cancellation; its timestamp does not determine the call's
|
|
243
|
+
serialized winner. Completion directly rechecks source state, including after
|
|
244
|
+
decoding or exceptional codec/transport paths. HTTP response receipt alone
|
|
245
|
+
cannot complete the client. Instrumentation and callbacks run outside state
|
|
246
|
+
locks, and only the executing fibers complete instrumentation. Original failures
|
|
247
|
+
remain available to lifecycle observers; normalized canceled/deadline/transport
|
|
248
|
+
errors have fixed safe messages and no sensitive rescued cause.
|
|
249
|
+
|
|
250
|
+
`TransportRequest.deadline` remains the actual Deadline or nil.
|
|
251
|
+
`TransportRequest.cancellation` implements `_Cancellation`: `cancelled?`,
|
|
252
|
+
`reason`, `check!`, and `on_cancel`. Transports only observe this call-owned SPI;
|
|
253
|
+
they cannot complete the client. Server accepts the same read-only seam and
|
|
254
|
+
creates its own processing arbitration. Testing forwards it without completing
|
|
255
|
+
the client's state. Context exposes its combined reason, with no token reader.
|
|
256
|
+
|
|
257
|
+
NetHTTP installs socket ownership before startup, attaches raw TCP at the TLS
|
|
258
|
+
hook or after plain startup, and closes a newly exposed socket immediately if
|
|
259
|
+
cancellation already won. TLS/read/write/header waits are interruptible; native
|
|
260
|
+
DNS/TCP before exposure remains cooperative. Per-call retirement prevents late
|
|
261
|
+
callbacks from accessing completed/recycled resources. Nil deadlines have no
|
|
262
|
+
watchdog or wire timeout; the exact independent native cap remains
|
|
263
|
+
`9_999_999.999`. Retry/reconnect guards and TLS verification remain enabled.
|
|
264
|
+
|
|
265
|
+
Async foreign-thread listeners only resolve a tagged Promise. Its owner reactor
|
|
266
|
+
arbitrates, stops private operation/setup tasks and performs bounded cleanup.
|
|
267
|
+
Promise resolution order does not decide the RPC outcome. Healthy stream reset
|
|
268
|
+
preserves the shared connection and siblings; a stuck deferred frame retains the one
|
|
269
|
+
100 ms cleanup budget and raw-first whole-connection fallback.
|
|
270
|
+
|
|
271
|
+
Native HTTP/2 creates a request cancellation source before processing,
|
|
272
|
+
independently of instrumentation. Only peer RST_STREAM with CANCEL (0x8) signals
|
|
273
|
+
it, before upstream input teardown can resume reads. Exceptional input paths
|
|
274
|
+
record their original failure and check cancellation before normalization.
|
|
275
|
+
Processing detaches this source at completion; delivery observation continues
|
|
276
|
+
independently. NO_ERROR/other resets, END_STREAM, half-close, EOF, connection loss
|
|
277
|
+
and host/fatal-owner abort add no explicit cancellation guarantee. Portable
|
|
278
|
+
Rack/Rails and native Falcon HTTP/1 add no token injection or peer detection.
|
|
279
|
+
|
|
280
|
+
## Evidence and future direction
|
|
281
|
+
|
|
282
|
+
Specs exercise real custom bindings/transports/invokers, ProtoJSON mapping,
|
|
283
|
+
Rack::Lint success and errors, real localhost HTTP, byte limits and lifecycle.
|
|
284
|
+
The pinned connect-go Client and Handler verify both cross-language directions
|
|
285
|
+
and codecs against one shared descriptor fixture, including details,
|
|
286
|
+
host-visible metadata and a request limit. The same Go client scenarios also
|
|
287
|
+
run against the Go server as a reference baseline, with raw repeated Go fields
|
|
288
|
+
instead of WEBrick's coalesced values. There are 18 fixture cases: six reference
|
|
289
|
+
baseline cases and twelve cross-language cases. This is a unary subset, not
|
|
290
|
+
full conformance. The separate [official conformance harness](../conformance/README.md)
|
|
291
|
+
requires 96 client and 90 server identity passes, including two GET successes
|
|
292
|
+
in each direction. A separate gzip inventory requires 88 client and 82 server
|
|
293
|
+
passes. All four caller-cancellation cases pass unwaived with actual request
|
|
294
|
+
writes and socket cleanup, for 184 client and 172 server passes in total.
|
|
295
|
+
Separate Go gzip scenarios
|
|
296
|
+
exercise both directions/codecs, negotiation, errors and bounds with wire
|
|
297
|
+
observations. The pinned Go client rewrites accept preferences, so independent
|
|
298
|
+
raw Go HTTP requests additionally prove gzip-request/identity-response behavior.
|
|
299
|
+
The pinned Go server prefers request gzip over explicit identity acceptance;
|
|
300
|
+
Ruby clients reject that unadvertised response. There is no full-conformance claim.
|
|
301
|
+
|
|
302
|
+
Wire protocol, HTTP transport, and message binding remain separate choices.
|
|
303
|
+
[Native gRPC](grpc.md) and [gRPC-Web](grpc-web.md) have distinct implemented
|
|
304
|
+
profiles. Interceptors and Faraday remain future work; production readiness
|
|
305
|
+
remains subject to the support contract's adoption gates. Unsupported
|
|
306
|
+
modes/options are rejected rather than represented as buffered streaming.
|
|
307
|
+
|
|
308
|
+
The optional [connectrpc-on-rails gem](../gems/connectrpc-on-rails/README.md) uses the
|
|
309
|
+
invoker boundary to run native controller callbacks, authentication,
|
|
310
|
+
`rescue_from` and instrumentation after bounded core decode. It depends on `connectrpc-rack` for Rack translation and keeps hosting
|
|
311
|
+
dependencies out of core. It preserves native
|
|
312
|
+
CSRF/session policy, reports unexpected errors through Rails.error and keeps
|
|
313
|
+
generated files non-reloadable outside Zeitwerk paths. A mounted Rack app alone
|
|
314
|
+
does not supply the Rails controller lifecycle.
|
|
315
|
+
See the [Rails controller guide](https://guides.rubyonrails.org/action_controller_overview.html).
|
|
316
|
+
|
|
317
|
+
The optional [connectrpc-async package](../gems/connectrpc-async/README.md)
|
|
318
|
+
selects Async HTTP/2 for standalone unary/streaming hosting and a matching transport.
|
|
319
|
+
It uses the existing transport/server seams, generated definitions and Google
|
|
320
|
+
binding without changing core Context or wire contracts. The application owns
|
|
321
|
+
the reactor and close lifecycle. Private, version-bounded wrappers own writes,
|
|
322
|
+
stream cancellation, request tasks and wire-complete drain; no frame/HPACK
|
|
323
|
+
engine or global monkeypatch is added. Real h2c and verified TLS `h2` are
|
|
324
|
+
required. Full TLS snapshots enable required client authentication, same-listener
|
|
325
|
+
credential reload and older-generation retirement. The exact-task client leaf
|
|
326
|
+
accessor is the optional authorization seam; peer-presented chains are not
|
|
327
|
+
claimed to be verified chains. See the package guide for session isolation,
|
|
328
|
+
cleanup's 100ms allowance and deliberate whole-connection failure for a stuck
|
|
329
|
+
frame. Native streaming uses the same ingress and cleanup boundaries. No production daemon claim is made.
|
|
330
|
+
The H2 client's private, reactor-owned connection manager runs shared connection
|
|
331
|
+
attempts outside every RPC lifecycle and owns stream accounting, capped jittered
|
|
332
|
+
backoff, opt-in wait-for-ready and count/request-byte admission. A call returns
|
|
333
|
+
to acquisition only when its connection became unusable before any stream ID or
|
|
334
|
+
request byte; nothing that may have reached the server is resent. The package
|
|
335
|
+
guide describes the policy, states and observer events.
|
|
336
|
+
|
|
337
|
+
Connect streaming carries end-stream metadata in its body; native gRPC requires
|
|
338
|
+
actual final HTTP/2 HEADERS, as described in the [gRPC guide](grpc.md).
|
|
339
|
+
For Connect, the Async backend preserves leading repeated fields and leading
|
|
340
|
+
`trailer-*` metadata while ignoring actual trailing HEADERS after backend validation.
|
|
341
|
+
It reconstructs lengths normalized by the backend; malformed lengths and some
|
|
342
|
+
singleton repeats can fail before core. Conservative frame/window settings do
|
|
343
|
+
not provide a hard process-memory/header-size guarantee. Independent connect-go
|
|
344
|
+
unary h2c/TLS/mTLS results are separate from core's identity and gzip conformance
|
|
345
|
+
profiles. Combined gzip specs exercise opaque bytes and wire/decompressed bounds
|
|
346
|
+
over the actual Async host and transport. NetHTTP remains HTTP/1.1; each future
|
|
347
|
+
transport needs its own verified capability scope.
|
|
348
|
+
|
|
349
|
+
The reusable `ConnectRPC::Async::Application` translates native Protocol::HTTP
|
|
350
|
+
requests independently of hosting. `TLSConfiguration` owns complete versioned
|
|
351
|
+
provider results and immutable worker-local compilation; `ServerSession` owns
|
|
352
|
+
raw-accept generation capture, setup/admission, exact-task identity and reviewed
|
|
353
|
+
HTTP/2 resource cleanup. The strict standalone Host composes that session.
|
|
354
|
+
The optional [Falcon package](../gems/connectrpc-falcon/README.md) composes it with
|
|
355
|
+
Falcon's process workers, raw endpoints and HTTP server/run lifecycle. Its local
|
|
356
|
+
HTTP/1 resource uses upstream parse/write hooks and preserves wire completion
|
|
357
|
+
and callback boundaries during retirement. A private response-body EOF marker
|
|
358
|
+
pairs with the pinned writer's next successful final flush to establish one
|
|
359
|
+
absolute 500 ms final-close expiry before callbacks. Successfully delivered
|
|
360
|
+
retiring Connect responses half-close raw TCP writes, retain the original
|
|
361
|
+
100 ms / 256 KiB bounded discard, then wait only the remaining total budget
|
|
362
|
+
without further reads. Admission/socket ownership joins original processing and
|
|
363
|
+
retirement; owner shutdown can shorten either phase. These cooperative timers
|
|
364
|
+
can overshoot under scheduler delay and do not guarantee peer receipt. The
|
|
365
|
+
independent Web rejected-input policy is unchanged. Only explicit Rack fallback
|
|
366
|
+
receives Falcon's generic encoding/cache middleware. Native Connect streaming hosts
|
|
367
|
+
select explicit core shape capabilities. See the package guide for source failure, expiry, worker
|
|
368
|
+
stop and unsupported socket-ownership transfer policies.
|
|
369
|
+
|
|
370
|
+
The explicit optional `connectrpc/generator` entry point implements the standard
|
|
371
|
+
protoc request/response protocol through official `google/protobuf/plugin_pb`.
|
|
372
|
+
The extensionless executable reads raw protobuf to EOF, with no conformance
|
|
373
|
+
length framing. Malformed protobuf, missing/incompatible Google and CLI arguments
|
|
374
|
+
fail on stderr with nonzero status and empty stdout. Unsupported descriptors
|
|
375
|
+
return `CodeGeneratorResponse.error`, no partial files and exit zero. Unexpected
|
|
376
|
+
implementation exceptions remain visible. Only `FEATURE_PROTO3_OPTIONAL` is
|
|
377
|
+
advertised; editions are unsupported.
|
|
378
|
+
|
|
379
|
+
Each service-bearing target emits `_connect.rb`, requiring core, its official
|
|
380
|
+
`_pb.rb` and any additionally referenced message files. Imported streaming
|
|
381
|
+
services are not targets. Generated modules
|
|
382
|
+
contain frozen RPC declaration constants, `SERVICE`, a `Client` initialized with
|
|
383
|
+
`client:`, and `.handlers` with required callable RPC keywords. Unary wrappers forward `call(METHOD, request, **options)` unchanged. Streaming
|
|
384
|
+
wrappers forward the matching scoped operation, its required block and all options.
|
|
385
|
+
Partial handlers/custom invocation remain core Server features. No framework,
|
|
386
|
+
transport configuration or binding auto-require is added.
|
|
387
|
+
Optional [RBS signatures](rbs.md) preserve these runtime boundaries and require
|
|
388
|
+
`rbs=true` for service signature generation.
|
|
389
|
+
Concrete types come from the official generated descriptor pool, preserving
|
|
390
|
+
nested classes, imports, proto2/proto3 presence and `ruby_package` mappings.
|
|
391
|
+
|
|
392
|
+
Naming follows protoc 36.2's
|
|
393
|
+
[RubifyConstant and PackageToModule](https://github.com/protocolbuffers/protobuf/blob/v36.2/src/google/protobuf/compiler/ruby/ruby_generator.cc):
|
|
394
|
+
message/service names preserve underscores and use `PB_` for a nonalphabetic
|
|
395
|
+
initial character; package underscores delimit capitalized words. An explicit
|
|
396
|
+
`ruby_package` containing `::` is used component-wise; otherwise dot/package
|
|
397
|
+
conversion applies. RPC names use the gRPC Ruby acronym and lower/digit-to-upper
|
|
398
|
+
underscore rule. Keyword/inherited wrapper names (including private copy/object
|
|
399
|
+
hooks and implicit conversions), leading-underscore RPCs,
|
|
400
|
+
`BEGIN`/`END`, reserved constants, unsafe paths, unresolved/non-message types,
|
|
401
|
+
duplicates and namespace/output collisions fail before emitting files.
|
|
402
|
+
All runtime constant references are absolute. Namespace checks index the request's
|
|
403
|
+
package/message/enum graph and target services, including Ruby aliases and
|
|
404
|
+
class/module conflicts. Fixed runtime protection applies to package ancestors,
|
|
405
|
+
official message/enum assignments and target service declarations in the loaded
|
|
406
|
+
import/type closure. Compatible `Google`/`Google::Protobuf` package modules may
|
|
407
|
+
be shared, including equivalent `ruby_package` mappings. Direct replacement of
|
|
408
|
+
those modules, reserved roots such as `ConnectRPC`/`File`, and concrete protobuf
|
|
409
|
+
runtime names such as `Google::Protobuf::DescriptorPool` are rejected. Known
|
|
410
|
+
official protobuf types are accepted only under their canonical file, proto name
|
|
411
|
+
and Ruby namespace, preserving WKT imports without allowing application types to
|
|
412
|
+
replace them. The fixed policy follows the pinned protobuf sources, rather than
|
|
413
|
+
reflecting the generator process's constants. A service cannot merge with
|
|
414
|
+
another package. No global registry spans generator invocations.
|
|
415
|
+
Generated files guard directly defined `SERVICE`, `Client` and RPC constants;
|
|
416
|
+
normal `require` is idempotent, while deliberate `load`/redefinition fails clearly.
|
|
417
|
+
Application constants outside the request still need compatible namespaces.
|
|
418
|
+
|
|
419
|
+
A Rust descriptor-driven
|
|
420
|
+
backend using [prost-reflect](https://docs.rs/prost-reflect/latest/prost_reflect/)
|
|
421
|
+
remains an option; cross-backend message-class compatibility is not promised.
|
|
422
|
+
No custom protobuf parser is implemented. See the
|
|
423
|
+
[Ruby generated-code guide](https://protobuf.dev/reference/ruby/ruby-generated/)
|
|
424
|
+
and [Rack specification](https://github.com/rack/rack/blob/v3.2.7/SPEC.rdoc).
|
|
425
|
+
|
|
426
|
+
See the optional [in-process testing package](../gems/connectrpc-testing/README.md) for real encoded client/server round trips without sockets.
|
|
427
|
+
|
|
428
|
+
## RPC lifecycle observation
|
|
429
|
+
|
|
430
|
+
`Instrumentation.new(subscribers:, on_error: nil)` freezes a copied subscriber
|
|
431
|
+
list. `start(call)` returns a per-call observer. Optional callbacks are fixed:
|
|
432
|
+
`attach` returns a local activation token; `detach(token)` unwinds successful
|
|
433
|
+
attachments in reverse registration order; `processing_finished(outcome)`
|
|
434
|
+
receives an immutable final processing snapshot; `finished(completion)` receives
|
|
435
|
+
one terminal processing/delivery observation. Core owns one application yield.
|
|
436
|
+
Each callback/reporter StandardError is isolated. Every successful attachment
|
|
437
|
+
token receives one detach attempt, including when another detach raises a
|
|
438
|
+
fatal exception or throws to a matching catch. Returning detaches preserve the
|
|
439
|
+
escaping exit; competing cleanup exits follow ordinary Ruby ensure semantics.
|
|
440
|
+
Observers must make attach/detach exception-safe: the dispatcher cannot repair
|
|
441
|
+
private context state mutated before a failing attach returns a token or
|
|
442
|
+
guarantee that a failing detach restores its own state. This cleanup does not
|
|
443
|
+
guarantee completion under arbitrary repeated asynchronous interruption.
|
|
444
|
+
Retained observers hold no globally active core context.
|
|
445
|
+
|
|
446
|
+
Call facts contain role, Connect protocol, recognized immutable Method or nil,
|
|
447
|
+
client endpoint host/port and a propagation carrier, never message objects.
|
|
448
|
+
The client starts before timeout/metadata validation and applies validated staged
|
|
449
|
+
injection transactionally to copied metadata. Server extraction uses defensive
|
|
450
|
+
read-only headers without moving validation ahead of 405. Outcomes distinguish
|
|
451
|
+
original exceptions, normalized/replacement exceptions, emitted Connect code,
|
|
452
|
+
HTTP status, monotonic processing duration and measured encoded byte counts.
|
|
453
|
+
Unknown byte counts are nil. Metadata snapshots retain repeated arrays.
|
|
454
|
+
|
|
455
|
+
`Server#start_call(path:, headers:, http_method: "POST")` is the explicit host
|
|
456
|
+
SPI. Hosts pass the actual method so Connect GET selection precedes subscriber
|
|
457
|
+
start, regardless of ignored POST content headers. It resolves a known method
|
|
458
|
+
and starts observers without reading input or validating protocol. The default
|
|
459
|
+
preserves POST behavior for existing callers; subclass overrides must accept
|
|
460
|
+
and forward the new keyword.
|
|
461
|
+
`Server#call(..., lifecycle: handle)` consumes that Server/path-bound handle once
|
|
462
|
+
and leaves terminal ownership to the host. `handle.activate { ... }` scopes
|
|
463
|
+
context to the executing task; `handle.finish(delivery:, error: nil,
|
|
464
|
+
decision: :processed, http_status: nil)` terminates idempotently. Ordinary
|
|
465
|
+
Server#call owns start/processing/unobserved completion. Optional
|
|
466
|
+
`TransportRequest.lifecycle` lets a transport activate independent RPC tasks.
|
|
467
|
+
`Context#record_failure(error)` retains the first original escaping failure
|
|
468
|
+
before framework normalization. It does not itself complete a call.
|
|
469
|
+
|
|
470
|
+
Processing state (`not_started`, `running`, `finished`), admission/host decision
|
|
471
|
+
and delivery observation are independent. A host abort while processing runs
|
|
472
|
+
is latched until processing cleanup, with processing callback before terminal
|
|
473
|
+
callback. Terminal state is committed before observers run, preventing reentry.
|
|
474
|
+
Admission-only calls have no fabricated processing result. Portable Rack
|
|
475
|
+
response-finished callbacks take precedence over body close and have their own
|
|
476
|
+
label; they never claim native full-write completion. A consumer that neither
|
|
477
|
+
closes a body nor runs promised callbacks can leave observation unfinished.
|
|
478
|
+
Native Async/Falcon own explicit request exchanges before admission, independent
|
|
479
|
+
handler activation and write/reset/cleanup evidence. Connection setup before an
|
|
480
|
+
RPC exists remains outside this scope. See the
|
|
481
|
+
[OTel guide](../gems/connectrpc-otel/README.md) for trace semantics, privacy,
|
|
482
|
+
HTTP instrumentation nesting and synchronous processor costs.
|
|
483
|
+
|
|
484
|
+
## Explicit HTTP/1.1 client ownership
|
|
485
|
+
|
|
486
|
+
The Async package's independent `HTTP1Transport` uses exclusive reactor-owned
|
|
487
|
+
leases for Connect unary, safe GET and SS/CS. Reservations precede yielding
|
|
488
|
+
TCP/TLS setup; retiring resources count toward the cap until raw close and task
|
|
489
|
+
joins finish. Idle reuse requires normal writer completion, full HTTP EOF,
|
|
490
|
+
successful cleanup and native persistent/idle state. A nonblocking acquisition
|
|
491
|
+
probe retires FIN or unsolicited data before dispatch. No failed call is replayed.
|
|
492
|
+
|
|
493
|
+
A private H1 stream separates response reads from output flushing and caps native
|
|
494
|
+
body fragments at 16 KiB. Strict HTTP/1.1 status syntax preserves three-digit
|
|
495
|
+
extension statuses for core fallback. Framing validation precedes native header
|
|
496
|
+
mutation; leading metadata is snapshotted before chunk trailers. Core retains
|
|
497
|
+
terminal decoding and send/error arbitration. Complete unary non-200 responses
|
|
498
|
+
stop unfinished uploads destructively and return to core; HTTP 200 requires
|
|
499
|
+
writer success. Cleanup failures cannot turn into successful completion.
|
|
500
|
+
|
|
501
|
+
TLS offers only `http/1.1`, accepts nil or H1 ALPN, and rejects other negotiation
|
|
502
|
+
before dispatch. Hostname/peer verification and mTLS remain enabled. The shared
|
|
503
|
+
private TLS helper retains H2's existing default. H1 changes no server runtime
|
|
504
|
+
policy, H2 connection ownership, or Web rejection behavior.
|