connectrpc-on-rails 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 +256 -0
- data/lib/connectrpc/on_rails/controller.rb +137 -0
- data/lib/connectrpc/on_rails/version.rb +7 -0
- data/lib/connectrpc/on_rails.rb +6 -0
- data/lib/connectrpc-on-rails.rb +3 -0
- data/rbs/action_controller_8/controller.rbs +11 -0
- data/sig/connectrpc-on-rails.rbs +22 -0
- data/sig/manifest.yaml +3 -0
- metadata +99 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: bf9035d7ed20c0a0fa34ca686bfebec9849f32a2826905050af3912ffd5ec8f1
|
|
4
|
+
data.tar.gz: 5458391b5e2eceb1238a7caaf27b5b92d1f16857cbccb83d882925adae706297
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a790bb7f79b68215639bc012e66c26472505454cad2e30ec171bf346eb1163b700c3627135bb67d4fe5013dffd1bb41fa65cebce06f1dfcff82ee6b75589c113
|
|
7
|
+
data.tar.gz: 5ed934edc8ce7a0cc60a3b93d5299a05efc25c6fe11589c9180bbc1c03b9f0c0b694d1f7781483193172ec6906fd68ca1175ad33737143c85326448afdecb6f7
|
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,256 @@
|
|
|
1
|
+
# connectrpc-on-rails
|
|
2
|
+
|
|
3
|
+
Optional native Rails controller execution for the experimental, unofficial
|
|
4
|
+
ConnectRPC unary Ruby gem. The core gem remains independent of Rails and has
|
|
5
|
+
zero runtime dependencies. This adapter reuses its Server invoker and the
|
|
6
|
+
optional [connectrpc-rack](../connectrpc-rack/README.md) translation. It does not add another protocol implementation.
|
|
7
|
+
|
|
8
|
+
See the [supported 0.x contract](../../docs/support.md) for the target support
|
|
9
|
+
profile, matched package versions, compatibility policy and adoption gates.
|
|
10
|
+
|
|
11
|
+
## Use a controller
|
|
12
|
+
|
|
13
|
+
Add `connectrpc-on-rails` and your chosen protobuf binding dependency to your
|
|
14
|
+
application Gemfile. Runtime dependencies are `connectrpc ~> 0.1.0`,
|
|
15
|
+
`connectrpc-rack ~> 0.1.0`, and `actionpack >= 8.0.5.1, < 8.2`.
|
|
16
|
+
The Rack package supplies the `rack >= 3.2.7, < 4` requirement. The application
|
|
17
|
+
chooses its controller base, binding, authentication, and security policy.
|
|
18
|
+
Requiring `connectrpc/on_rails` loads neither `ActionController::Base` nor
|
|
19
|
+
`ActionController::API`, and does not load Google protobuf. `Bundler.require`
|
|
20
|
+
loads the same entry point through `connectrpc-on-rails`.
|
|
21
|
+
|
|
22
|
+
Generate official protobuf messages and Connect service glue as described in
|
|
23
|
+
the [root README](../../README.md). Keep generated files outside Zeitwerk's
|
|
24
|
+
managed paths. For example, use `generated/rpc`, add that directory to
|
|
25
|
+
`$LOAD_PATH`, and require `echo_connect` from an initializer. If placing generated
|
|
26
|
+
files under `lib`, explicitly ignore their directory in the application's
|
|
27
|
+
Zeitwerk configuration before eager loading. Generated filenames do not match
|
|
28
|
+
the constants they define, generated glue requires its message files, and it
|
|
29
|
+
intentionally rejects `load`/redefinition. These files are non-reloadable.
|
|
30
|
+
Ordinary application controllers retain normal Rails reload behavior.
|
|
31
|
+
|
|
32
|
+
For the repository's generated `Example::V1::EchoService` fixture:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
require "connectrpc/on_rails"
|
|
36
|
+
require "connectrpc/bindings/google_protobuf"
|
|
37
|
+
require "echo_connect"
|
|
38
|
+
|
|
39
|
+
class EchoController < ApplicationController
|
|
40
|
+
include ConnectRPC::OnRails::Controller
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def connect_service = Example::V1::EchoService::SERVICE
|
|
45
|
+
def connect_binding = ConnectRPC::Bindings::GoogleProtobuf.new
|
|
46
|
+
def connect_handlers
|
|
47
|
+
Example::V1::EchoService.handlers(echo: method(:rpc_echo), fail: method(:rpc_fail))
|
|
48
|
+
end
|
|
49
|
+
def connect_limits = { request_limit: 1_048_576 }
|
|
50
|
+
|
|
51
|
+
def rpc_echo(message, context)
|
|
52
|
+
context.response_metadata.add("x-worker", "rails")
|
|
53
|
+
context.response_trailers.add("x-finished", "yes")
|
|
54
|
+
message
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def rpc_fail(_message, _context)
|
|
58
|
+
raise ConnectRPC::Error.new(:invalid_argument, "safe application message")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Use normal named controller routing, optionally under a literal scope:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
scope "/rpc" do
|
|
67
|
+
match "/example.v1.EchoService(/*rpc)", to: "echo#connect", via: :all, format: false
|
|
68
|
+
end
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`via: :all` lets core return the protocol's unsupported-verb response.
|
|
72
|
+
`format: false` preserves dots in service names. Dynamic scope segments are
|
|
73
|
+
not supported. Route/service mismatches raise a configuration error.
|
|
74
|
+
The adapter strips only the matched route's literal prefix. Unknown RPC names,
|
|
75
|
+
deeper suffixes, method case and raw escapes reach core unchanged after that
|
|
76
|
+
boundary. Rails itself normalizes duplicate/trailing slashes and escape case
|
|
77
|
+
before controller dispatch, so path acceptance differs from a direct Rack mount.
|
|
78
|
+
|
|
79
|
+
Only `connect` is an action supplied by the concern. Keep the three required
|
|
80
|
+
configuration hooks and typed handlers private, using prefixed handler names to
|
|
81
|
+
avoid controller/Kernel method collisions. `connect_limits` is optional and
|
|
82
|
+
accepts core `request_limit:`, `response_limit:`, `request_wire_limit:`,
|
|
83
|
+
`response_wire_limit:`, `get_query_limit:` and `encoding:` keywords; omitted options
|
|
84
|
+
use core's 4 MiB defaults. Private `connect_request`, `connect_context`, and
|
|
85
|
+
`connect_method` expose the same typed objects to callbacks and handlers.
|
|
86
|
+
Handlers return typed messages. There is no render helper or routing DSL.
|
|
87
|
+
|
|
88
|
+
## Lifecycle and errors
|
|
89
|
+
|
|
90
|
+
Core negotiates, validates deadlines and bounds, reads, and decodes before
|
|
91
|
+
running native Rails callbacks. Authentication callbacks therefore run after
|
|
92
|
+
up to 4 MiB of input has been decoded by default. Preflight failures do not run
|
|
93
|
+
controller callbacks or emit `process_action.action_controller` events.
|
|
94
|
+
Callbacks, `rescue_from`, around-action cleanup and instrumentation use Rails'
|
|
95
|
+
real `process_action` chain. Native completion occurs before core serializes
|
|
96
|
+
the typed result. Configuration, handlers, server and context are evaluated
|
|
97
|
+
fresh for each request. Synchronize any application-owned shared state.
|
|
98
|
+
|
|
99
|
+
Raise `ConnectRPC::Error` to reject a request or map an application exception.
|
|
100
|
+
Its deliberate code, safe message, typed details, metadata and trailers survive.
|
|
101
|
+
If the application has a catch-all rescue, declare a reraising handler after
|
|
102
|
+
it because native Rails rescue precedence favors later declarations:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
rescue_from StandardError, with: :handle_application_error
|
|
106
|
+
rescue_from ConnectRPC::Error do |error|
|
|
107
|
+
raise error
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Raw native render/head halts and exceptions covered by Rails'
|
|
112
|
+
`rescue_responses` use only the unambiguous inverse of Connect HTTP statuses:
|
|
113
|
+
401/403/404/429/499/501/503/504 become unauthenticated/permission_denied/
|
|
114
|
+
not_found/resource_exhausted/canceled/unimplemented/unavailable/
|
|
115
|
+
deadline_exceeded. Messages are generic. Ambiguous 400/409/500, redirects,
|
|
116
|
+
unmapped statuses, and success without a typed result become `internal`.
|
|
117
|
+
A non-200 native halt after the handler also prevents typed success. Raw Rails
|
|
118
|
+
bodies and exception messages never become protocol error messages.
|
|
119
|
+
Exceptions explicitly registered in Rails' `rescue_responses` remain expected
|
|
120
|
+
outcomes and do not produce adapter error reports or exception logs. Unexpected
|
|
121
|
+
exceptions are logged with their original backtraces and reported through
|
|
122
|
+
`Rails.error` before safe core normalization. Request preparation uses the same
|
|
123
|
+
classification, including application overrides to `rescue_responses`.
|
|
124
|
+
|
|
125
|
+
Controller instrumentation describes controller execution: a deliberate Connect
|
|
126
|
+
exception can appear as a Rails 500, while successful controller execution can
|
|
127
|
+
precede a core encode or response-limit failure. Deadlines remain cooperative;
|
|
128
|
+
they cannot interrupt arbitrary application code or blocked synchronous reads.
|
|
129
|
+
Warden/Devise throws and failure-app responses remain host-owned non-Connect
|
|
130
|
+
responses unless the application explicitly adapts them.
|
|
131
|
+
|
|
132
|
+
## Request and security policy
|
|
133
|
+
|
|
134
|
+
During Connect execution, Rails receives an empty request-local indifferent body
|
|
135
|
+
parameter cache and an explicit JSON format. This prevents ParamsWrapper and
|
|
136
|
+
instrumentation from rereading ProtoJSON scalar/array/object roots or parsing
|
|
137
|
+
malformed Accept headers. Use typed `connect_request` for message fields;
|
|
138
|
+
query and route parameters remain accessible by string and symbol. Ordinary
|
|
139
|
+
non-Connect actions retain normal body parsing and format negotiation.
|
|
140
|
+
|
|
141
|
+
The adapter does not skip CSRF protection, disable sessions, or alter cookie
|
|
142
|
+
policy. A dedicated API controller suits token-authenticated RPC. Base
|
|
143
|
+
controllers use Rails' native safe-verb policy: GET does not require a CSRF
|
|
144
|
+
token; POST retains its configured token/session checks. The application must
|
|
145
|
+
explicitly route desired GET calls and preserve authentication/authorization. An
|
|
146
|
+
application may choose a narrowly scoped exemption according to its security
|
|
147
|
+
model; inclusion alone grants none.
|
|
148
|
+
|
|
149
|
+
Final core status, wire content type and emitted headers replace the interim
|
|
150
|
+
Rails representation. Stale content-length, content-encoding, ETag,
|
|
151
|
+
Last-Modified and Location are removed. Other host headers, default security
|
|
152
|
+
headers, CSP, authentication headers and cookies survive; repeated Rack 3
|
|
153
|
+
values remain arrays. Downstream Rails/Rack middleware retains its normal
|
|
154
|
+
ownership of request ids, ETags, content length and other final host behavior.
|
|
155
|
+
|
|
156
|
+
## Verification and tested combinations
|
|
157
|
+
|
|
158
|
+
The target supported Rails profile is 8.1. Rails 8.0 remains compatibility
|
|
159
|
+
evidence outside that profile. The tested combinations use Linux x64 and
|
|
160
|
+
Bundler 4.0.23:
|
|
161
|
+
|
|
162
|
+
| Rails | Ruby | Target profile status |
|
|
163
|
+
| --- | --- | --- |
|
|
164
|
+
| 8.1.4 | 3.4.12 and 4.0.9 | Supported framework profile |
|
|
165
|
+
| 8.0.5.1 | 3.4.12 | Compatibility evidence only |
|
|
166
|
+
|
|
167
|
+
These rows do not promise every framework/runtime combination. As checked on
|
|
168
|
+
2026-11-02, Rails'
|
|
169
|
+
[maintenance policy](https://rubyonrails.org/maintenance) lists security support
|
|
170
|
+
ending 2026-11-07 for 8.0 and 2027-10-10 for 8.1. The 8.0 compatibility lane
|
|
171
|
+
is not a promise to extend upstream maintenance.
|
|
172
|
+
|
|
173
|
+
From the repository root:
|
|
174
|
+
|
|
175
|
+
```sh
|
|
176
|
+
mise tasks
|
|
177
|
+
RAILS_LANE=8.0 mise run test:rails
|
|
178
|
+
RAILS_LANE=8.1 mise run verify:rails
|
|
179
|
+
MISE_RUBY_VERSION=4.0.9 RAILS_LANE=8.1 mise run test:rails
|
|
180
|
+
mise run generation:check:rails
|
|
181
|
+
mise run check
|
|
182
|
+
mise run verify
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`setup:rails`, `test:rails`, `generation:check:rails`, `build:rails`,
|
|
186
|
+
`check:package:rails`, and `verify:rails` select `RAILS_LANE=8.1` by default.
|
|
187
|
+
Tests accept RSpec paths/options relative to this adapter directory.
|
|
188
|
+
Actual Appraisal 2.5.0 runs under the selected frozen lane bundle, with no extra
|
|
189
|
+
base lock. `BUNDLE_COOLDOWN=7` applies to every adapter bundle process; the plain
|
|
190
|
+
Gemfile source is necessary because Appraisal cannot parse source keywords.
|
|
191
|
+
Pinned Bundler must exist before Appraisal can run, preventing its legacy
|
|
192
|
+
installer fallback. Never use `appraisal install` in CI. Generation is checked
|
|
193
|
+
in a temporary relative-layout copy, without rewriting tracked files.
|
|
194
|
+
|
|
195
|
+
Both lane locks include checksums and `ruby`/`x86_64-linux` platforms. Native
|
|
196
|
+
variants recorded by Bundler remain checksum-locked. New resolved artifacts
|
|
197
|
+
require a primary registry release-age and archive SHA256 audit. Runtime cache
|
|
198
|
+
uses root `vendor/bundle` by ABI; adapter artifacts and coverage live under
|
|
199
|
+
`vendor/rails`. SimpleCov starts before adapter requires and includes every
|
|
200
|
+
adapter library file, independently of core coverage.
|
|
201
|
+
|
|
202
|
+
`check:package:rails` builds all three core/Rack/Rails archives, checks exact manifests and metadata,
|
|
203
|
+
and installs them with conventional offline dependency resolution into fresh
|
|
204
|
+
GEM_HOME/GEM_PATH outside the checkout. Installed generated clients perform
|
|
205
|
+
binary/JSON success and structured error calls through a real Rails application
|
|
206
|
+
and Rack::Lint, loading integration code only from the installed archive.
|
|
207
|
+
See [development](../../docs/development.md) for the separate core protocol,
|
|
208
|
+
generation, Go interoperability and official conformance evidence.
|
|
209
|
+
|
|
210
|
+
## Status and licensing
|
|
211
|
+
|
|
212
|
+
Experimental and not production ready. Streaming, native gRPC and production
|
|
213
|
+
hosting are outside this adapter. Explicit binary/text unary gRPC-Web uses the
|
|
214
|
+
private hooks described below.
|
|
215
|
+
Original project work is [MIT licensed](LICENSE), copyright 2026 Jim Myhrberg.
|
|
216
|
+
Both gem archives include the complete license and pin `allowed_push_host` to
|
|
217
|
+
`https://rubygems.org`; only the [release workflow](../../docs/releasing.md)
|
|
218
|
+
publishes them. Verification authorizes no publication.
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
To enable unary gzip, return `encoding: :gzip` from the private `connect_limits`
|
|
222
|
+
hook. Core validates wire length, inflates under the decoded request limit, and
|
|
223
|
+
decodes before callbacks and native instrumentation. JSON scalar bodies still
|
|
224
|
+
bypass Rails body-parameter parsing. Negotiated success responses are compressed
|
|
225
|
+
after native execution; errors remain identity. A compression/size failure after
|
|
226
|
+
execution does not rerun callbacks, and native process-action instrumentation can
|
|
227
|
+
record 200 before core replaces the response with its final error. Wire limits
|
|
228
|
+
default to the corresponding message limits; gzip framing overhead must fit too.
|
|
229
|
+
|
|
230
|
+
## RPC observation
|
|
231
|
+
|
|
232
|
+
Configure `instrumentation:` on the core Client/Server to enable the fixed
|
|
233
|
+
neutral per-call lifecycle or the optional [OpenTelemetry adapter](../connectrpc-otel/README.md).
|
|
234
|
+
Native controllers override private `connect_instrumentation` (default nil),
|
|
235
|
+
separately from `connect_limits`. Escaping original failures are captured before
|
|
236
|
+
reporting/normalization; successfully handled `rescue_from` exceptions are not
|
|
237
|
+
automatically failures. Configuration before Server construction is outside it.
|
|
238
|
+
|
|
239
|
+
## Static signatures
|
|
240
|
+
|
|
241
|
+
This package owns `sig/` and its discovery manifest. Add
|
|
242
|
+
`library "connectrpc-on-rails"` to a strict Steep target with this gem installed.
|
|
243
|
+
See the [RBS guide](../../docs/rbs.md) for supported APIs, extension contracts,
|
|
244
|
+
partial records, runtime validation and framework-signature limits.
|
|
245
|
+
`mise run rbs:check` checks application callers for this package; strict core
|
|
246
|
+
source checking is a separate target from backend implementation checking.
|
|
247
|
+
|
|
248
|
+
The finite upstream compatibility signatures in `rbs/action_controller_8` are explicit
|
|
249
|
+
opt-ins, separate from normal gem discovery. See the [RBS guide](../../docs/rbs.md#opt-in-framework-compatibility)
|
|
250
|
+
for tested versions, supported framework forms and how to avoid duplicate
|
|
251
|
+
application-owned declarations.
|
|
252
|
+
|
|
253
|
+
## gRPC-Web
|
|
254
|
+
|
|
255
|
+
Private connect_protocol, connect_status_codec and connect_cors hooks enable binary/text unary gRPC-Web through the real controller invoker. POST/OPTIONS routes are explicit; auth, callbacks, cookies, security headers and Rails CSRF/origin controls remain active.
|
|
256
|
+
See the [Web guide](../../docs/grpc-web.md) for protocol, CORS, bounds and completion rules.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ConnectRPC
|
|
4
|
+
module OnRails
|
|
5
|
+
module Controller # rubocop:disable Metrics/ModuleLength
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
# Only this method is a Rails action. The generated handler map stays private.
|
|
9
|
+
def connect
|
|
10
|
+
handler = @connect_handlers[connect_method.path]
|
|
11
|
+
raise Error.new(:unimplemented, "Unknown method") unless handler
|
|
12
|
+
|
|
13
|
+
@connect_result = handler.call(connect_request, connect_context)
|
|
14
|
+
@connect_result_set = true
|
|
15
|
+
self.response_body = ""
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
attr_reader :connect_request, :connect_context, :connect_method
|
|
21
|
+
|
|
22
|
+
def connect_service = raise NotImplementedError, "Implement private connect_service"
|
|
23
|
+
def connect_binding = raise NotImplementedError, "Implement private connect_binding"
|
|
24
|
+
def connect_handlers = raise NotImplementedError, "Implement private connect_handlers"
|
|
25
|
+
def connect_limits = {}
|
|
26
|
+
def connect_instrumentation = nil
|
|
27
|
+
def connect_protocol = :connect
|
|
28
|
+
def connect_status_codec = nil
|
|
29
|
+
def connect_cors = nil
|
|
30
|
+
|
|
31
|
+
def process_action(name, ...)
|
|
32
|
+
return super unless name.to_s == "connect"
|
|
33
|
+
|
|
34
|
+
@connect_handlers = connect_handlers
|
|
35
|
+
connect_dispatch do |rpc_method, message, context|
|
|
36
|
+
@connect_method = rpc_method
|
|
37
|
+
@connect_request = message
|
|
38
|
+
@connect_context = context
|
|
39
|
+
connect_run_lifecycle { super(name, ...) }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def connect_dispatch(&invoker)
|
|
44
|
+
service = connect_service
|
|
45
|
+
env = connect_env(service)
|
|
46
|
+
server = Server.new(services: [service], binding: connect_binding, handlers: @connect_handlers,
|
|
47
|
+
invoker: invoker, instrumentation: connect_instrumentation,
|
|
48
|
+
protocol: connect_protocol, status_codec: connect_status_codec, cors: connect_cors,
|
|
49
|
+
**connect_limits)
|
|
50
|
+
ConnectRPC::Rack.new(server: server).__send__(:transfer_response, env) do |result, cors_owned|
|
|
51
|
+
connect_apply_response(result, cors_owned: cors_owned)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def connect_env(service)
|
|
56
|
+
prefix = connect_route_prefix(service)
|
|
57
|
+
raw_path = request.get_header("PATH_INFO")
|
|
58
|
+
boundary = "#{prefix}/#{service.name}"
|
|
59
|
+
unless raw_path == boundary || raw_path.start_with?("#{boundary}/")
|
|
60
|
+
raise ArgumentError, "Connect route does not match the configured service"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
request.env.merge("PATH_INFO" => raw_path.delete_prefix(prefix))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def connect_route_prefix(service)
|
|
67
|
+
# Use the matched route's literal prefix, never decoded wildcard parameters.
|
|
68
|
+
pattern = request.route_uri_pattern.to_s
|
|
69
|
+
match = %r{\A([^:*()]*)/#{Regexp.escape(service.name)}(?=/|\(|\z)}.match(pattern)
|
|
70
|
+
raise ArgumentError, "Connect route must name the configured service after a literal scope" unless match
|
|
71
|
+
|
|
72
|
+
match[1]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def connect_prepare_request
|
|
76
|
+
request.request_parameters = ActiveSupport::HashWithIndifferentAccess.new
|
|
77
|
+
request.formats = [:json]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def connect_run_lifecycle # rubocop:disable Metrics/AbcSize
|
|
81
|
+
connect_prepare_request
|
|
82
|
+
yield
|
|
83
|
+
raise connect_status_error(response.status) unless @connect_result_set && response.status == 200
|
|
84
|
+
|
|
85
|
+
@connect_result
|
|
86
|
+
rescue Error
|
|
87
|
+
raise
|
|
88
|
+
rescue StandardError => e
|
|
89
|
+
connect_context.record_failure(e)
|
|
90
|
+
connect_report(e)
|
|
91
|
+
raise connect_status_error(ActionDispatch::ExceptionWrapper.status_code_for_exception(e.class.name))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def connect_report(error)
|
|
95
|
+
return if ActionDispatch::ExceptionWrapper.rescue_responses.key?(error.class.name)
|
|
96
|
+
|
|
97
|
+
logger&.error { "[connectrpc.rails] #{error.full_message(highlight: false)}" }
|
|
98
|
+
return unless defined?(::Rails) && ::Rails.respond_to?(:error)
|
|
99
|
+
|
|
100
|
+
::Rails.error.report(error, handled: false, source: "connectrpc.rails")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def connect_status_error(status)
|
|
104
|
+
# HTTP_FALLBACK is a client proxy mapping and is deliberately unsuitable here.
|
|
105
|
+
codes = Error::HTTP_STATUS.select { |_code, value| value == status }.keys
|
|
106
|
+
code = codes.one? ? codes.first : "internal"
|
|
107
|
+
Error.new(code, code == "internal" ? "Internal error" : code.tr("_", " ").capitalize)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def connect_apply_response(result, cors_owned:) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
111
|
+
status, headers, body = result
|
|
112
|
+
if cors_owned && response.headers.any? do |key, values|
|
|
113
|
+
key.start_with?("access-control-") && Array(values) != headers[key]
|
|
114
|
+
end
|
|
115
|
+
raise Error.new(:internal, "Conflicting application CORS policy")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
%w[content-length content-encoding etag last-modified location].each { |key| response.delete_header(key) }
|
|
119
|
+
self.status = status
|
|
120
|
+
headers.each do |key, values|
|
|
121
|
+
values = connect_vary_header(values) if key == "vary"
|
|
122
|
+
# Rails parses Content-Type as a scalar. Repeated metadata remains Rack 3 arrays.
|
|
123
|
+
response.set_header(key, key == "content-type" ? values.fetch(0) : values)
|
|
124
|
+
end
|
|
125
|
+
self.response_body = body
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def connect_vary_header(values)
|
|
129
|
+
native = Array(response.get_header("vary")).flat_map { |value| value.split(",").map(&:strip) }
|
|
130
|
+
return native if native.include?("*")
|
|
131
|
+
|
|
132
|
+
incoming = values.flat_map { |value| value.split(",").map(&:strip) }
|
|
133
|
+
(native + incoming).uniq(&:downcase)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Opt in only when the application has no Action Controller signatures.
|
|
2
|
+
# Tested with actionpack 8.0.5.1 and 8.1.4. This is a nominal inheritance seam
|
|
3
|
+
# and the named callback form, not a declaration of the complete Rails API.
|
|
4
|
+
module ActionController
|
|
5
|
+
class API
|
|
6
|
+
def self.before_action: (*Symbol) -> void
|
|
7
|
+
end
|
|
8
|
+
class Base
|
|
9
|
+
def self.before_action: (*Symbol) -> void
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module ConnectRPC
|
|
2
|
+
module OnRails
|
|
3
|
+
VERSION: String
|
|
4
|
+
module Controller
|
|
5
|
+
type limits = { ?request_limit: Integer, ?response_limit: Integer, ?request_wire_limit: Integer, ?response_wire_limit: Integer, ?encoding: :identity | :gzip, ?get_query_limit: Integer?, ?grpc_status_wire_limit: Integer, ?grpc_status_limit: Integer }
|
|
6
|
+
def connect: () -> String
|
|
7
|
+
private
|
|
8
|
+
def connect_service: () -> Service
|
|
9
|
+
def connect_binding: () -> _Binding
|
|
10
|
+
def connect_handlers: () -> Hash[String, ^(untyped, Context) -> untyped | _Handler[untyped, untyped]]
|
|
11
|
+
def connect_limits: () -> limits
|
|
12
|
+
def connect_protocol: () -> server_protocol_selection
|
|
13
|
+
def connect_status_codec: () -> _GRPCStatusCodec?
|
|
14
|
+
def connect_cors: () -> GRPCWebCORS?
|
|
15
|
+
def connect_instrumentation: () -> Instrumentation?
|
|
16
|
+
# Outside active decoded execution these readers have no established value.
|
|
17
|
+
attr_reader connect_request: Object?
|
|
18
|
+
attr_reader connect_context: Context?
|
|
19
|
+
attr_reader connect_method: Method[untyped, untyped]?
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
data/sig/manifest.yaml
ADDED
metadata
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: connectrpc-on-rails
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- jimeh
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 2026-11-15 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: actionpack
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 8.0.5.1
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '8.2'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: 8.0.5.1
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '8.2'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: connectrpc
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - "~>"
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: 0.1.0
|
|
39
|
+
type: :runtime
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - "~>"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: 0.1.0
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: connectrpc-rack
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - "~>"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: 0.1.0
|
|
53
|
+
type: :runtime
|
|
54
|
+
prerelease: false
|
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - "~>"
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: 0.1.0
|
|
60
|
+
description: Optional, experimental Rails lifecycle integration for the unofficial
|
|
61
|
+
ConnectRPC Ruby gem.
|
|
62
|
+
executables: []
|
|
63
|
+
extensions: []
|
|
64
|
+
extra_rdoc_files: []
|
|
65
|
+
files:
|
|
66
|
+
- LICENSE
|
|
67
|
+
- README.md
|
|
68
|
+
- lib/connectrpc-on-rails.rb
|
|
69
|
+
- lib/connectrpc/on_rails.rb
|
|
70
|
+
- lib/connectrpc/on_rails/controller.rb
|
|
71
|
+
- lib/connectrpc/on_rails/version.rb
|
|
72
|
+
- rbs/action_controller_8/controller.rbs
|
|
73
|
+
- sig/connectrpc-on-rails.rbs
|
|
74
|
+
- sig/manifest.yaml
|
|
75
|
+
homepage: https://github.com/jimeh/connectrpc-ruby
|
|
76
|
+
licenses:
|
|
77
|
+
- MIT
|
|
78
|
+
metadata:
|
|
79
|
+
allowed_push_host: https://rubygems.org
|
|
80
|
+
rubygems_mfa_required: 'true'
|
|
81
|
+
source_code_uri: https://github.com/jimeh/connectrpc-ruby
|
|
82
|
+
rdoc_options: []
|
|
83
|
+
require_paths:
|
|
84
|
+
- lib
|
|
85
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '3.4'
|
|
90
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - ">="
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: '0'
|
|
95
|
+
requirements: []
|
|
96
|
+
rubygems_version: 3.6.9
|
|
97
|
+
specification_version: 4
|
|
98
|
+
summary: Native Rails controller execution for Connect unary RPCs
|
|
99
|
+
test_files: []
|