connectrpc 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +501 -0
  4. data/docs/design.md +504 -0
  5. data/docs/development.md +566 -0
  6. data/docs/grpc-web.md +165 -0
  7. data/docs/grpc.md +75 -0
  8. data/docs/rbs.md +469 -0
  9. data/docs/streaming.md +251 -0
  10. data/exe/protoc-gen-connectrpc-ruby +23 -0
  11. data/lib/connectrpc/bindings/descriptors/status.bin +15 -0
  12. data/lib/connectrpc/bindings/google_protobuf.rb +92 -0
  13. data/lib/connectrpc/cancellation.rb +220 -0
  14. data/lib/connectrpc/client.rb +260 -0
  15. data/lib/connectrpc/compression.rb +120 -0
  16. data/lib/connectrpc/deadline.rb +81 -0
  17. data/lib/connectrpc/error.rb +72 -0
  18. data/lib/connectrpc/generator/names.rb +105 -0
  19. data/lib/connectrpc/generator/rbs_renderer.rb +93 -0
  20. data/lib/connectrpc/generator/renderer.rb +115 -0
  21. data/lib/connectrpc/generator/runtime_names.rb +84 -0
  22. data/lib/connectrpc/generator/schema.rb +218 -0
  23. data/lib/connectrpc/generator/streaming_methods.rb +29 -0
  24. data/lib/connectrpc/generator.rb +42 -0
  25. data/lib/connectrpc/get_query.rb +159 -0
  26. data/lib/connectrpc/grpc.rb +200 -0
  27. data/lib/connectrpc/grpc_client.rb +23 -0
  28. data/lib/connectrpc/grpc_server.rb +158 -0
  29. data/lib/connectrpc/grpc_web.rb +131 -0
  30. data/lib/connectrpc/grpc_web_cors.rb +135 -0
  31. data/lib/connectrpc/grpc_web_io.rb +156 -0
  32. data/lib/connectrpc/grpc_web_server.rb +207 -0
  33. data/lib/connectrpc/instrumentation.rb +216 -0
  34. data/lib/connectrpc/message_io.rb +47 -0
  35. data/lib/connectrpc/metadata.rb +105 -0
  36. data/lib/connectrpc/method.rb +68 -0
  37. data/lib/connectrpc/protocol.rb +189 -0
  38. data/lib/connectrpc/server.rb +398 -0
  39. data/lib/connectrpc/streaming/client.rb +104 -0
  40. data/lib/connectrpc/streaming/client_execution.rb +87 -0
  41. data/lib/connectrpc/streaming/client_scope.rb +105 -0
  42. data/lib/connectrpc/streaming/framing.rb +123 -0
  43. data/lib/connectrpc/streaming/framing_io.rb +61 -0
  44. data/lib/connectrpc/streaming/method.rb +33 -0
  45. data/lib/connectrpc/streaming/server.rb +122 -0
  46. data/lib/connectrpc/streaming/server_execution.rb +88 -0
  47. data/lib/connectrpc/streaming/server_terminal.rb +70 -0
  48. data/lib/connectrpc/streaming/session.rb +143 -0
  49. data/lib/connectrpc/streaming/session_completion.rb +134 -0
  50. data/lib/connectrpc/streaming/session_io.rb +107 -0
  51. data/lib/connectrpc/streaming/session_messages.rb +119 -0
  52. data/lib/connectrpc/streaming/strategy.rb +95 -0
  53. data/lib/connectrpc/streaming/streams.rb +116 -0
  54. data/lib/connectrpc/streaming/terminal.rb +52 -0
  55. data/lib/connectrpc/transports/net_http.rb +246 -0
  56. data/lib/connectrpc/version.rb +5 -0
  57. data/lib/connectrpc.rb +38 -0
  58. data/rbs/google_protobuf_4_36/runtime.rbs +41 -0
  59. data/sig/connectrpc.rbs +772 -0
  60. data/sig/manifest.yaml +2 -0
  61. data/third_party/README.md +16 -0
  62. data/third_party/google/LICENSE +201 -0
  63. data/third_party/google/rpc/status.proto +48 -0
  64. data/third_party/protobuf-LICENSE +32 -0
  65. metadata +106 -0
data/docs/grpc-web.md ADDED
@@ -0,0 +1,165 @@
1
+ # gRPC-Web servers
2
+
3
+ The [supported 0.x contract](support.md) defines the target compatibility
4
+ boundary and the remaining adoption gates.
5
+
6
+ Enable gRPC-Web explicitly on the existing server. Generated declarations,
7
+ message bindings, unary invokers and server-stream handlers remain protocol neutral.
8
+ There is no Ruby gRPC-Web client or new runtime dependency.
9
+
10
+ ```ruby
11
+ binding = ConnectRPC::Bindings::GoogleProtobuf.new
12
+ cors = ConnectRPC::GRPCWebCORS.new(
13
+ origins: ["https://app.example"], credentials: true,
14
+ request_headers: %w[authorization x-csrf-token], expose_headers: ["x-request-id"]
15
+ )
16
+ server = ConnectRPC::Server.new(
17
+ services: [service], handlers: handlers, binding: binding,
18
+ protocol: %i[connect grpc grpc_web], cors: cors
19
+ )
20
+ ```
21
+
22
+ `protocol: :grpc_web` selects Web alone. A protocol array is copied and frozen;
23
+ it must contain distinct `:connect`, `:grpc` or `:grpc_web` symbols. The default
24
+ is `:connect`. `:auto` remains exactly Connect and native gRPC. Both gRPC families
25
+ require `_GRPCStatusCodec`, supplied by the Google binding or a separate
26
+ `status_codec:` composed with a custom protobuf binding. Enabling Web on a server
27
+ does not change `Client(protocol: :connect | :grpc)` or its default transport.
28
+
29
+ When Connect is enabled, an actual GET selects Connect before observation,
30
+ native publication and Web CORS. Query controls override ignored POST content
31
+ headers, including a supplied Web Content-Type. Safe unary methods use the
32
+ existing Connect GET path; unsafe methods return 405 without handler dispatch.
33
+ A Web-only server still classifies Web GET as Web and rejects it with 405.
34
+
35
+ | Host | Unary binary/text | Server-stream binary/text wire | Incremental browser stream |
36
+ | --- | --- | --- | --- |
37
+ | Async native H2 | Yes | Yes | Text |
38
+ | Falcon native H2 and H1 | Yes | Yes | Text |
39
+ | Rack with rackup/WEBrick | Yes | Rejected before input | No |
40
+ | Native Rails controller | Yes | Rejected before callbacks | No |
41
+ | Testing manual encoded `TransportRequest` | Yes | Rejected before input | No |
42
+
43
+ Official grpc-web 2.1.1 browser clients support binary unary, text unary and
44
+ incremental text server streaming. Binary XHR buffers server-stream responses.
45
+ Binary server-stream wire interoperability uses independent connect-go. Client
46
+ streaming, bidi, JSON, per-message compression and whole-body compression are
47
+ outside this profile. Puma has not been verified. Browser TLS fixtures use an
48
+ explicit test-only verification exception; verified TLS/mTLS wire checks are
49
+ separate. Browser client-certificate selection is not promised.
50
+
51
+ ## Media, framing and errors
52
+
53
+ Requests accept exactly `application/grpc-web[+proto]` or
54
+ `application/grpc-web-text[+proto]`, case-insensitively, without parameters.
55
+ Responses include `+proto`. POST is required. An independent bounded Accept
56
+ list supports these aliases, `application/*`, `*/*` and standard q values. The
57
+ most specific match determines each representation's quality. Highest quality
58
+ wins; the request representation wins ties. Responses vary on Accept.
59
+
60
+ Exactly one framed protobuf request and request EOF precede invocation. Empty
61
+ protobuf is a message; missing or excess requests are UNIMPLEMENTED. Excess
62
+ framing is detected without decoding a second message. Reserved flags, request
63
+ terminal, truncation, malformed protobuf/base64 and malformed Content-Length
64
+ are INVALID_ARGUMENT. Unnegotiated compressed DATA is INTERNAL. Unsupported
65
+ valid grpc-encoding tokens are UNIMPLEMENTED; malformed/repeated encoding is
66
+ INTERNAL. Web remains identity-only on a mixed server configured with gzip.
67
+
68
+ Before negotiation, 405/Allow, 415/Accept-Post and 406 are bare HTTP responses.
69
+ After negotiation, RPC errors use HTTP 200 and one 0x80 body terminal. The
70
+ terminal contains lower-case CRLF field lines, exactly one grpc-status and no
71
+ extra empty line. Repeated trailers remain separate wire lines; the official
72
+ JS runtime collapses duplicate trailer keys. Rich errors use the canonical
73
+ private Any/Status codec described in [native gRPC](grpc.md). All six grpc-timeout
74
+ units work; zero expires immediately and absence means no logical expiry.
75
+
76
+ ## CORS and Rails
77
+
78
+ CORS is disabled by default. One owner controls each mount. `GRPCWebCORS` is an
79
+ immutable configuration value with exact serialized ASCII HTTP(S) origins.
80
+ It accepts no wildcard, regex, callback or null origin. At most 32 distinct
81
+ origins of 2 KiB each and 32 additional token header names of 128 bytes each are
82
+ accepted. Credential sharing defaults false. Explicit additions are required
83
+ for auth, CSRF and trace request headers.
84
+
85
+ Owned OPTIONS preflight returns empty 204 or 403 before RPC observation, body
86
+ reads, binding or application callbacks. Requested method must be POST and
87
+ requested names must be allowed. Max-Age is zero. Actual Web requests without
88
+ Origin are allowed; disallowed or ambiguous origins receive 403 before input.
89
+ Allowed-origin policy also decorates RPC errors, negotiation failures and native
90
+ admission 503. Vary merges Origin, Accept and preflight controls without replacing
91
+ application cache, cookie or security fields. Conflicting application
92
+ access-control fields fail closed before publication. Set `cors: nil` when outer
93
+ middleware already owns CORS. CORS does not authenticate or replace CSRF checks.
94
+
95
+ Native Rails overrides stay private:
96
+
97
+ ```ruby
98
+ def connect_protocol = %i[connect grpc_web]
99
+ def connect_status_codec = connect_binding
100
+ def connect_cors = application_web_cors
101
+ def connect_limits = { grpc_status_wire_limit: 65_536, grpc_status_limit: 65_536 }
102
+ ```
103
+
104
+ Explicitly route the intended POST and OPTIONS paths. The normal controller
105
+ invoker, callbacks, authentication, sessions, cookies, CSRF and security headers
106
+ remain active. Preflight does not bypass actual POST checks. A valid CSRF token
107
+ does not override Rails' origin policy. No CSRF exemption is installed.
108
+
109
+ ## Bounds and host SPI
110
+
111
+ Message and independent message-wire limits default to 4 MiB. Framing adds five
112
+ bytes. Binary request entity cap is `5 + request_wire_limit`; text entity cap is
113
+ `4 * (5 + request_wire_limit)` because independently padded one-byte groups can
114
+ expand fourfold. Declared and actual encoded bytes, decoded framing and message
115
+ bytes are checked independently. Incremental decoding retains at most three
116
+ encoded carry characters plus a bounded decoded fragment. Whitespace, bad
117
+ alphabet, padding, pad bits and incomplete quartets are rejected.
118
+
119
+ `grpc_status_wire_limit` and `grpc_status_limit` default to 64 KiB. Terminal
120
+ serialization checks both actual CRLF bytes and name/value/32-byte field
121
+ accounting. Rich status has an independent decoded cap. Oversize status becomes
122
+ bounded RESOURCE_EXHAUSTED, or aborts if even the minimal terminal cannot fit.
123
+ Text terminal staging is separately bounded by base64 expansion. Portable unary
124
+ uses a finite aggregate response cap; native streaming never aggregates a whole
125
+ response. Native output retains one queued and one in-flight 16 KiB entity
126
+ fragment. Text uses raw slices of at most 12,288 bytes and flushes padded groups
127
+ at existing write boundaries, including the first small prefix.
128
+
129
+ Native H2 ingress remains 65,536 bytes/five entries with 16 KiB chunks and
130
+ consumption-based stream credit. The H1 input adapter may retain one pinned
131
+ backend chunk of up to 64 KiB. During rejected-input cleanup this remainder can
132
+ coexist with one discard read of at most 64 KiB. Complete parsed field blocks are checked before application copies;
133
+ H2 includes pseudo/status fields. Parser/readahead, HPACK/CONTINUATION temporaries,
134
+ codec allocations, CPU and total RSS are outside these retention bounds.
135
+
136
+ Custom hosts call `Server#call_grpc_web(path:, http_method:, headers:, input:,
137
+ output:, publish:, capabilities: [:unary], lifecycle: nil, cancellation: nil)`.
138
+ Streaming requires explicit `:server_stream` capability. Input and publication
139
+ reuse `_StreamingInput` and `StreamHead`; `_GRPCWebOutput` provides bounded
140
+ `write(bytes, timeout:)`, `finish(terminal:)` and `abort`. Finish only stages
141
+ prepared bytes. Hosts must hold **all terminal bytes** until the original
142
+ processor and outer application cleanup succeed, then release those bytes.
143
+ Body consumers perform no handler, codec, status or base64 work. Cleanup failure
144
+ discards the candidate terminal and aborts delivery. Admission includes original
145
+ processing and wire/callback completion. H1 has explicit resources, not fake H2
146
+ streams. Only an actual H2 CANCEL reset signals Context cancellation. H1 EOF,
147
+ disconnect and host shutdown do not. There is no Connect one-second tail policy.
148
+ Falcon native H1 now has a separate explicit rejected-input resource policy:
149
+ one absolute 100 ms allowance to finish the original HTTP body framing, shortened
150
+ by the original valid grpc-timeout and subject to earlier cancellation, drain
151
+ and owner abort. Each discard read is at most 64 KiB; progress does not reset the
152
+ budget and a following request is never consumed. Expiry or malformed framing
153
+ retires that connection raw-first, preserving the original RPC outcome and
154
+ independent aborted delivery. This best effort does not promise delivery for
155
+ arbitrarily slow uploads or extend existing cleanup allowances. See the
156
+ [Falcon guide](../gems/connectrpc-falcon/README.md#grpc-web).
157
+
158
+ `Server#cors_response(path:, http_method:, headers:, response: nil)` is a pure
159
+ typed host SPI for early decisions and idempotent decoration, including admission
160
+ 503. Call it before lifecycle start for preflight or denied requests.
161
+
162
+ The optional OTel adapter maps Web to the project's gRPC semconv 1.44 mapping,
163
+ with `rpc.system.name=grpc` and bounded `connectrpc.protocol=grpc_web`. Call.protocol
164
+ is fixed before subscriber start. Processing, admission, delivery and original
165
+ failures remain separate; SDK/exporter ownership and privacy are unchanged.
data/docs/grpc.md ADDED
@@ -0,0 +1,75 @@
1
+ # Native gRPC
2
+
3
+ The [supported 0.x contract](support.md) defines the target compatibility
4
+ boundary and the remaining adoption gates.
5
+
6
+ Configure `Client(protocol: :grpc)` with an Async transport and a protobuf
7
+ binding. Unary, server streaming, client streaming and bidi use the existing
8
+ method declarations, generated clients and scoped stream endpoints. Configure
9
+ `Server(protocol: :grpc)` for gRPC alone, or `protocol: :auto` for exactly Connect
10
+ and gRPC. Async and Falcon HTTP/2 hosts share the native execution engine.
11
+ h2c, verified TLS and mTLS use the existing backend configuration.
12
+
13
+ The default remains Connect. gRPC accepts `application/grpc` and
14
+ `application/grpc+proto`, and emits the latter. It supports protobuf, identity and
15
+ opt-in per-message gzip. JSON and GET are rejected eagerly. NetHTTP, Rack, Rails,
16
+ Testing and Falcon HTTP/1 reject gRPC before message encoding or handler input.
17
+ Native handlers are plain handlers; native execution provides no Rails callback
18
+ or authentication execution contract.
19
+
20
+ Enabling gRPC requires a capable status codec at construction. A binding may
21
+ implement `_GRPCStatusCodec`, or callers can pass a separate
22
+ `status_codec:` with `encode_status(Error) -> String` and
23
+ `decode_status(String, code: Integer) -> Error`. The optional Google binding
24
+ provides it using canonical Any and Status descriptors in a private pool.
25
+ Opaque detail types and bytes survive without registering their descriptors.
26
+ Core has no protobuf runtime dependency.
27
+
28
+ `grpc_status_wire_limit` bounds the complete terminal field block, counting
29
+ name bytes, value bytes and 32 bytes per occurrence. `grpc_status_limit` bounds
30
+ decoded rich status bytes before protobuf parsing. Both default to 65,536.
31
+ Base64 expansion means a decoded status may exceed the independent wire limit.
32
+ Native gRPC always limits each complete decoded HEADERS block to 65,536 bytes,
33
+ including custom and informational fields, even with larger core configuration.
34
+ These are retention bounds applied before application copies, excluding HPACK,
35
+ CONTINUATION, parser temporary allocation, CPU and RSS. Message and independent
36
+ wire limits remain 4 MiB per message. Ingress and output bounds are unchanged.
37
+
38
+ Clients require actual final HTTP/2 HEADERS and stream EOF. Trailers-only means
39
+ initial final-response HEADERS with END_STREAM. Ordinary initial grpc-status is
40
+ not terminal. Server terminal fields are staged behind processor completion;
41
+ late application cleanup failure prevents successful delivery. Success includes
42
+ transport-owned cleanup, never peer receipt or TLS shutdown.
43
+
44
+ Status accepts a nonempty ASCII digit sequence with leading zeros, bounded by
45
+ the field block budget. Numeric values above uint32 map to unknown; there is no
46
+ 10-character spelling limit or unbounded integer parse. Output is canonical.
47
+ Rich status codes compare numerically before unknown-code normalization, and
48
+ rich messages take precedence over grpc-message. Invalid percent escapes remain
49
+ literal; decoded invalid UTF8 is scrubbed. As a deliberate stricter policy than
50
+ grpc-go, details on OK, malformed or duplicate details, and mismatched rich codes
51
+ produce safe internal instead of silently dropping rich data.
52
+
53
+ Missing server timeout means no logical expiry. gRPC timeout accepts all six
54
+ case-sensitive units and zero as immediately expired. Clients default to 30
55
+ seconds; nil omits the timeout and logical watchdog. Dispatch recomputes remaining
56
+ budget without extending monotonic expiry. gRPC per-operation waits have a
57
+ 9,999,999.999-second ceiling, further shortened by real deadlines. Connect Async
58
+ nil waits retain their existing behavior. There is no one-second gRPC tail timer.
59
+ Send-I/O terminal recovery has one absolute 100ms budget and allows at most
60
+ 262,144 additional body bytes, with separate finite header limits. It skips
61
+ messages without decoding or inflating them and cannot take over an active
62
+ reader. Only a validated remote error can replace an unhandled send failure;
63
+ remote success cannot. The existing destructive cleanup has a separate 100ms
64
+ budget. A stuck frame can fail its connection and siblings during that cleanup.
65
+
66
+ Only actual peer CANCEL resets signal server Context cancellation. Half-close,
67
+ EOF, other resets, connection loss and shutdown do not. No retry or replay occurs.
68
+ Client reset mapping follows grpc-go 1.85.0: FLOW_CONTROL_ERROR and
69
+ ENHANCE_YOUR_CALM map to resource_exhausted, REFUSED_STREAM to unavailable,
70
+ CANCEL to canceled, and INADEQUATE_SECURITY to permission_denied. Other resets
71
+ map to internal. The FLOW_CONTROL_ERROR choice differs from the older protocol
72
+ document's mapping. NO_ERROR can preserve queued data and a valid final status;
73
+ it cannot establish success without that status and owned transport completion.
74
+ Native unary executes the configured invoker with a normal Context, including
75
+ custom invokers that have no handler-map entry.