tonic-rails 0.5.0 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 234f197083161b48c0fa820f8e9e8bb20138208c131e8467279cd40d9b73d872
4
- data.tar.gz: a5ec7ea58b4325c881045c7550468a19de879b7119212e212500c4632e581f56
3
+ metadata.gz: 338a8abe3329b9c34aeb471e4b1b152696ae0a371ded6e375f2d42807775a8a2
4
+ data.tar.gz: 3ed3aedfabbde45d5ef7ed7598a2937f88232a0becaf93a0e3f17a9f8073503c
5
5
  SHA512:
6
- metadata.gz: 59c579e49bfc33f7f551ba522823fac67e253d939723c0fcdf4368560f558094b519a1339ca325d81cd2e06fa3dd51e1b7486b790b11da711ab04a24bfe2d375
7
- data.tar.gz: 229844b0f0d86b85f0545c715bb04c70148ecff829ea699283d5bf0a624aab91e788224af557ad513db559b3f42ed9c381350f0dd00238e0c78f0be67731437f
6
+ metadata.gz: eefe02d7dbed4dae8bf2d9af2743823877f8468d933b406f0e76f2b848de789c9dee0a95778804186c025aeffc60ea24dc28edc6f5645100e5c7d25dfd1c93ec
7
+ data.tar.gz: 2b9eaa3a25ca8fe7091bb761ddaf86a15564f187252045a81a24e8ce96f50da50479f2b2af108ccd5804c3c5b4ee3055000f54624fdc8f97a0aa223d8df421bd
data/CHANGELOG.md CHANGED
@@ -7,6 +7,47 @@ minor versions may contain breaking changes.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.7.0] - 2026-10-11
11
+
12
+ ### Added
13
+
14
+ - Client: opt-in retries, per client or per call (`retries: true` or a
15
+ Hash of `max_attempts`, `codes`, `initial_backoff`, `max_backoff` and
16
+ `multiplier`), following gRPC's retry design. Only calls the server
17
+ never started on are retried: a failed connection, or a status sent
18
+ without response headers. Waits are random with exponential caps, stop
19
+ before the deadline and follow `grpc-retry-pushback-ms`. Each retry is
20
+ logged, and `request_rpc.tonic_rails` reports `:attempts`. Closes #39.
21
+
22
+ ### Changed
23
+
24
+ - The server answers calls it turns away before a handler runs (shutting
25
+ down, dispatch queue full, malformed requests) with a Trailers-Only
26
+ response: the status in the headers and no body. Clients that follow
27
+ gRPC's retry rules can tell those calls never started and retry them
28
+ safely.
29
+
30
+ ## [0.6.0] - 2026-10-11
31
+
32
+ ### Added
33
+
34
+ - Client: `Tonic::Rails::Client` makes unary calls to other gRPC
35
+ services without the grpc gem. Stubs come from the descriptors that
36
+ `tonic_rails:compile_protos` generates. Calls take deadlines (and
37
+ default to the deadline of the RPC being handled), send metadata,
38
+ return headers and trailers with `return_call: true`, and raise
39
+ `RpcError` with rich details. TLS uses the system's root certificates,
40
+ a custom CA or mutual TLS. Clients keep working across `fork` with no
41
+ hooks, and `Timeout` and `Thread#raise` interrupt calls. Each call
42
+ publishes a `request_rpc.tonic_rails` event. Closes #37.
43
+ - Client: server-streaming calls. The stub method returns a stream that
44
+ yields each message as it arrives. A slow reader holds the server back
45
+ through HTTP/2 flow control, and stopping early (`break`, `first(n)`,
46
+ an exception) cancels the call. The deadline covers the whole stream,
47
+ an error partway through raises `RpcError` after the messages before
48
+ it, and `headers` and `trailers` are available once the stream ends.
49
+ Closes #38.
50
+
10
51
  ## [0.5.0] - 2026-10-11
11
52
 
12
53
  ### Added
@@ -145,7 +186,9 @@ First public release.
145
186
  - Precompiled native gems for Linux (x86_64, aarch64; glibc and musl) and
146
187
  macOS (x86_64, arm64), Ruby 3.3 to 4.0.
147
188
 
148
- [Unreleased]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.5.0...HEAD
189
+ [Unreleased]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.7.0...HEAD
190
+ [0.7.0]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.6.0...v0.7.0
191
+ [0.6.0]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.5.0...v0.6.0
149
192
  [0.5.0]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.4.0...v0.5.0
150
193
  [0.4.0]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.3.0...v0.4.0
151
194
  [0.3.0]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.2.0...v0.3.0
data/Cargo.lock CHANGED
@@ -155,6 +155,22 @@ dependencies = [
155
155
  "libloading",
156
156
  ]
157
157
 
158
+ [[package]]
159
+ name = "core-foundation"
160
+ version = "0.10.1"
161
+ source = "registry+https://github.com/rust-lang/crates.io-index"
162
+ checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
163
+ dependencies = [
164
+ "core-foundation-sys",
165
+ "libc",
166
+ ]
167
+
168
+ [[package]]
169
+ name = "core-foundation-sys"
170
+ version = "0.8.7"
171
+ source = "registry+https://github.com/rust-lang/crates.io-index"
172
+ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
173
+
158
174
  [[package]]
159
175
  name = "crc32fast"
160
176
  version = "1.5.2"
@@ -551,6 +567,12 @@ version = "1.21.4"
551
567
  source = "registry+https://github.com/rust-lang/crates.io-index"
552
568
  checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
553
569
 
570
+ [[package]]
571
+ name = "openssl-probe"
572
+ version = "0.2.1"
573
+ source = "registry+https://github.com/rust-lang/crates.io-index"
574
+ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
575
+
554
576
  [[package]]
555
577
  name = "parking_lot"
556
578
  version = "0.12.5"
@@ -759,6 +781,18 @@ dependencies = [
759
781
  "zeroize",
760
782
  ]
761
783
 
784
+ [[package]]
785
+ name = "rustls-native-certs"
786
+ version = "0.8.4"
787
+ source = "registry+https://github.com/rust-lang/crates.io-index"
788
+ checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
789
+ dependencies = [
790
+ "openssl-probe",
791
+ "rustls-pki-types",
792
+ "schannel",
793
+ "security-framework",
794
+ ]
795
+
762
796
  [[package]]
763
797
  name = "rustls-pki-types"
764
798
  version = "1.15.1"
@@ -779,12 +813,44 @@ dependencies = [
779
813
  "untrusted",
780
814
  ]
781
815
 
816
+ [[package]]
817
+ name = "schannel"
818
+ version = "0.1.29"
819
+ source = "registry+https://github.com/rust-lang/crates.io-index"
820
+ checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
821
+ dependencies = [
822
+ "windows-sys 0.61.2",
823
+ ]
824
+
782
825
  [[package]]
783
826
  name = "scopeguard"
784
827
  version = "1.2.0"
785
828
  source = "registry+https://github.com/rust-lang/crates.io-index"
786
829
  checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
787
830
 
831
+ [[package]]
832
+ name = "security-framework"
833
+ version = "3.7.0"
834
+ source = "registry+https://github.com/rust-lang/crates.io-index"
835
+ checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
836
+ dependencies = [
837
+ "bitflags",
838
+ "core-foundation",
839
+ "core-foundation-sys",
840
+ "libc",
841
+ "security-framework-sys",
842
+ ]
843
+
844
+ [[package]]
845
+ name = "security-framework-sys"
846
+ version = "2.17.0"
847
+ source = "registry+https://github.com/rust-lang/crates.io-index"
848
+ checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
849
+ dependencies = [
850
+ "core-foundation-sys",
851
+ "libc",
852
+ ]
853
+
788
854
  [[package]]
789
855
  name = "seq-macro"
790
856
  version = "0.3.6"
@@ -1041,9 +1107,10 @@ dependencies = [
1041
1107
 
1042
1108
  [[package]]
1043
1109
  name = "tonic_rails_native"
1044
- version = "0.5.0"
1110
+ version = "0.7.0"
1045
1111
  dependencies = [
1046
1112
  "axum",
1113
+ "base64",
1047
1114
  "bytes",
1048
1115
  "crossbeam-channel",
1049
1116
  "flate2",
@@ -1055,6 +1122,7 @@ dependencies = [
1055
1122
  "prost",
1056
1123
  "prost-types",
1057
1124
  "rb-sys",
1125
+ "rustls-native-certs",
1058
1126
  "socket2",
1059
1127
  "tokio",
1060
1128
  "tokio-stream",
data/README.md CHANGED
@@ -44,6 +44,10 @@ end
44
44
  INTERNAL error (the details go to your logs and `Rails.error`).
45
45
  Reflection is off unless you turn it on. TLS problems stop the server
46
46
  instead of silently falling back to plaintext.
47
+ - **A client too.** Make unary and server-streaming calls to other gRPC
48
+ services, with deadlines that follow your handler's, without the
49
+ `grpc` gem and its fork crashes. See
50
+ [Calling other services](#calling-other-services).
47
51
  - **No Rust toolchain needed** on the platforms we ship precompiled gems
48
52
  for: Linux and macOS (x86_64 and arm64) and Windows (x64).
49
53
 
@@ -401,6 +405,261 @@ const item = await client.getItem({ id: 1n });
401
405
  warning). `grpc_web_allowed_headers` adds request headers beyond the
402
406
  standard ones (`authorization` is already allowed).
403
407
 
408
+ ## Calling other services
409
+
410
+ tonic-rails includes a client for unary and server-streaming calls to
411
+ other gRPC services, so
412
+ an app that serves gRPC (or just calls it) doesn't need the `grpc` gem.
413
+ It runs on the same Rust stack as the server, and it keeps working after
414
+ `fork`. That matters because the `grpc` gem crashes or hangs in forked
415
+ processes (Puma with `preload_app!`, Sidekiq swarm, Resque) unless you
416
+ opt in to its Linux-only fork support and call `GRPC.prefork` and
417
+ friends around every fork yourself.
418
+
419
+ Create a client once and share it; it holds one HTTP/2 connection and is
420
+ safe to use from any thread:
421
+
422
+ ```ruby
423
+ # config/initializers/grpc_clients.rb
424
+ ITEMS = Tonic::Rails::Client.new("items.internal:50051", deadline: 2)
425
+ ```
426
+
427
+ Then get a stub for a service and call its methods, named in snake_case:
428
+
429
+ ```ruby
430
+ items = ITEMS.stub("demo.v1.ItemService")
431
+
432
+ item = items.get_item(Demo::V1::GetItemRequest.new(id: 1)) # a message
433
+ item = items.get_item(id: 1) # or its fields
434
+ item = items.get_item({ id: 1 }, deadline: 0.5, metadata: { "x-request-id" => request.request_id })
435
+ ```
436
+
437
+ Stubs are built from the protobuf descriptors in the `_pb.rb` files that
438
+ `tonic_rails:compile_protos` generates, the same ones your handlers use.
439
+ There's no `grpc_tools_ruby_protoc` step and no `_services_pb.rb`.
440
+ Responses are protobuf messages.
441
+
442
+ ### Server streaming
443
+
444
+ A server-streaming method returns a stream you read with `each`, or any
445
+ Enumerable method:
446
+
447
+ ```ruby
448
+ items.list_items(category: "books").each { |item| index(item) }
449
+ items.list_items(category: "books").first(10) # stops after ten
450
+ ```
451
+
452
+ - Nothing is sent until you start reading. Then each message is yielded
453
+ as it arrives, not when the stream ends.
454
+ - Stopping early, with `break`, `first(n)` or an exception, cancels the
455
+ call, and the server sees it cancelled.
456
+ - A slow reader holds the server back. A few messages are read ahead,
457
+ and HTTP/2 flow control stops the server after a couple of megabytes,
458
+ so a fast server can't fill your memory.
459
+ - If the server fails partway through, you get every message it sent
460
+ first, then `Tonic::Rails::RpcError`.
461
+ - Once the stream has ended, `headers` and `trailers` return its
462
+ metadata.
463
+ - A stream can be read once. Call the method again for a new one.
464
+
465
+ Client and bidirectional streaming aren't supported yet; their stub
466
+ methods raise `NotImplementedError`.
467
+
468
+ ### Deadlines
469
+
470
+ - `deadline:` on a call is seconds from now (`0.5`) or a `Time`. Without
471
+ one, the client's `deadline:` applies.
472
+ - **Inside a handler, outgoing calls default to the time left on the
473
+ handler's own deadline**, so a service that's out of time doesn't keep
474
+ its dependencies working on an answer nobody will read. If the client
475
+ also has a default deadline, the earlier of the two wins. A deadline on
476
+ the call itself always wins.
477
+ - When a deadline passes, the call raises `DEADLINE_EXCEEDED`, and the
478
+ server sees the call cancelled.
479
+ - A stream's deadline covers the whole stream, including the time your
480
+ block spends on each message. It's set when you call the method, so a
481
+ stream read later still keeps the handler's deadline.
482
+
483
+ Set a deadline. Without one, a call to a server that never answers waits
484
+ forever (or until `Timeout` interrupts it, which also works).
485
+
486
+ ### Errors
487
+
488
+ A call that fails raises `Tonic::Rails::RpcError`, the same class your
489
+ handlers raise:
490
+
491
+ ```ruby
492
+ begin
493
+ items.get_item(id: 1)
494
+ rescue Tonic::Rails::RpcError => e
495
+ e.code_name # :not_found, :unavailable, :deadline_exceeded, ...
496
+ e.message # the server's status message
497
+ e.details # rich error details (google.rpc.Status), as messages
498
+ e.metadata # trailing metadata
499
+ end
500
+ ```
501
+
502
+ Rich error details come back as their message class when its `_pb.rb` is
503
+ loaded, and as `Google::Protobuf::Any` otherwise. A server that can't be
504
+ reached raises `UNAVAILABLE`.
505
+
506
+ If a handler lets the error propagate, its caller receives the same
507
+ status, message, details and trailing metadata. Rescue it and raise your
508
+ own error when those are internal.
509
+
510
+ ### Retries
511
+
512
+ Retries are off unless you turn them on, for a client or a single call:
513
+
514
+ ```ruby
515
+ ITEMS = Tonic::Rails::Client.new("items.internal:50051", deadline: 2, retries: true)
516
+
517
+ items.get_item({ id: 1 }, retries: false) # not this call
518
+ items.get_item({ id: 1 }, retries: { max_attempts: 5 }) # or differently
519
+ ```
520
+
521
+ `retries: true` tries a call up to 3 times when it fails with
522
+ `UNAVAILABLE`. A Hash changes any of these:
523
+
524
+ | Field | Default | |
525
+ |---|---|---|
526
+ | `max_attempts` | 3 | attempts in all, from 2 to 5 |
527
+ | `codes` | `[:unavailable]` | status codes worth retrying |
528
+ | `initial_backoff` | 0.1 | seconds; the cap on the first wait |
529
+ | `max_backoff` | 1 | seconds; the cap on any wait |
530
+ | `multiplier` | 2 | how much the cap grows per attempt |
531
+
532
+ The rules come from gRPC's own retry design, so they avoid the usual
533
+ mistakes of a hand-written retry loop:
534
+
535
+ - **A call is retried only if the server never started on it.** That
536
+ means the connection failed (refused, reset, a pod restarting), or the
537
+ server answered with only a status and no response headers. Servers do
538
+ that when they turn a call away before any handler runs: a
539
+ tonic-rails server that's shutting down or whose queue is full, or a
540
+ proxy with no healthy backend. Once response headers arrive, the call
541
+ is never repeated, even if it then fails with a code you listed.
542
+ - **Waits are random.** Each one is anywhere from zero up to a cap that
543
+ starts at `initial_backoff` and grows by `multiplier` per attempt, up to
544
+ `max_backoff`. Clients that failed together don't retry together.
545
+ - **The deadline covers every attempt.** If the next attempt couldn't
546
+ start before the deadline, the client stops and raises the last
547
+ failure.
548
+ - **The server can steer.** A `grpc-retry-pushback-ms` trailer sets the
549
+ wait, and a negative one stops retries.
550
+ - A server stream is retried only before its first message.
551
+
552
+ Which calls are safe to retry? With the default `[:unavailable]`, nearly
553
+ all of them. By gRPC convention, `UNAVAILABLE` means the call can be tried
554
+ again, and from a tonic-rails server it's retried only when no handler
555
+ ran. For a tonic-rails server, `:resource_exhausted` is worth adding too:
556
+ a full dispatch queue returns it before any handler runs.
557
+
558
+ Take more care with codes a handler might raise after doing some work,
559
+ like `:aborted`. A tonic-rails server sends headers before it runs a
560
+ handler, so those calls still won't be retried. Other servers may answer
561
+ with only a status after a handler ran, so list those codes only for
562
+ methods that are safe to run twice.
563
+
564
+ Each retry is logged as a warning, through the client's `logger:` (by
565
+ default `Tonic::Rails.configuration.logger`, which is the Rails logger in
566
+ a Rails app):
567
+
568
+ ```
569
+ tonic-rails: /demo.v1.ItemService/GetItem failed with UNAVAILABLE: ... (attempt 1 of 3); retrying in 64ms
570
+ ```
571
+
572
+ ### Metadata
573
+
574
+ `metadata:` on a call, and on the client for every call, works like
575
+ response metadata in handlers: `-bin` names take binary values. To read
576
+ what the server sent back, pass `return_call: true`:
577
+
578
+ ```ruby
579
+ call = items.get_item({ id: 1 }, return_call: true)
580
+ call.response # the message
581
+ call.headers # response headers, -bin values decoded
582
+ call.trailers # trailing metadata
583
+ ```
584
+
585
+ ### TLS
586
+
587
+ TLS is on unless you turn it off, and plaintext needs an explicit
588
+ `tls: false`:
589
+
590
+ ```ruby
591
+ Tonic::Rails::Client.new("items.internal:50051") # system root certificates
592
+ Tonic::Rails::Client.new("items.internal:50051", tls_ca_path: "ca.pem") # your own CA instead
593
+ Tonic::Rails::Client.new("items.internal:50051", tls_ca_path: "ca.pem",
594
+ tls_cert_path: "client.pem", tls_key_path: "client.key") # mutual TLS
595
+ Tonic::Rails::Client.new("localhost:50051", tls: false) # plaintext
596
+ ```
597
+
598
+ `tls_server_name:` checks the server's certificate against another name
599
+ than the host you connect to. Unreadable certificates or keys raise when
600
+ the client is created, not on the first call.
601
+
602
+ ### Options
603
+
604
+ | Option | Default | |
605
+ |---|---|---|
606
+ | `tls` | `true` | `false` for plaintext |
607
+ | `tls_ca_path` | system roots | CA certificates (PEM) to trust instead |
608
+ | `tls_cert_path`, `tls_key_path` | none | client certificate and key (PEM), for mutual TLS |
609
+ | `tls_server_name` | the host | name to verify the server's certificate against |
610
+ | `deadline` | none | seconds each call may take, unless the call sets its own |
611
+ | `metadata` | `{}` | sent with every call |
612
+ | `max_message_size` | 4 MiB | largest message sent or received |
613
+ | `connect_timeout` | 5 | seconds to wait for a connection |
614
+ | `retries` | `false` | retry calls the server never started on; see [Retries](#retries) |
615
+ | `logger` | `Tonic::Rails.configuration.logger` | where retries are logged |
616
+
617
+ ### Fork safety
618
+
619
+ A client created before `fork` works in both processes, on Linux and
620
+ macOS, with nothing to call. Each process opens its own connection on
621
+ its first call, and a child never touches the threads or sockets it
622
+ inherited from its parent. Create clients in initializers, before Puma or
623
+ Sidekiq forks, and use them anywhere.
624
+
625
+ On macOS, also create the first client that uses the system's root
626
+ certificates before forking. Loading them goes through macOS's security
627
+ APIs, which abort in a process forked from a multi-threaded one. Loaded
628
+ once, they're kept and inherited, so clients created later in children
629
+ are fine. Linux has no such limit.
630
+
631
+ ### Migrating from the grpc gem
632
+
633
+ Call sites barely change; how you build the stub does:
634
+
635
+ ```ruby
636
+ # Before: grpc gem
637
+ stub = Demo::V1::ItemService::Stub.new("items.internal:50051", GRPC::Core::ChannelCredentials.new)
638
+ item = stub.get_item(Demo::V1::GetItemRequest.new(id: 1), deadline: Time.now + 2, metadata: { "x-id" => "1" })
639
+
640
+ # After: tonic-rails
641
+ stub = Tonic::Rails::Client.new("items.internal:50051").stub("demo.v1.ItemService")
642
+ item = stub.get_item(Demo::V1::GetItemRequest.new(id: 1), deadline: Time.now + 2, metadata: { "x-id" => "1" })
643
+ ```
644
+
645
+ - `stub` also accepts the grpc gem's service class
646
+ (`Demo::V1::ItemService::Service`), which helps while both gems are
647
+ installed.
648
+ - `deadline:` takes a `Time`, as before, or seconds.
649
+ - `return_op: true` becomes `return_call: true`, which returns the
650
+ response with its headers and trailers instead of an operation.
651
+ - `GRPC::BadStatus` subclasses become one `Tonic::Rails::RpcError`:
652
+ `rescue GRPC::NotFound` becomes `rescue Tonic::Rails::RpcError => e`
653
+ and a check of `e.code_name == :not_found`. The `raise_grpc_error`
654
+ matcher and `assert_grpc_error` accept both.
655
+ - `:this_channel_is_insecure` becomes `tls: false`.
656
+ - Server-streaming methods return a stream instead of an Enumerator.
657
+ Code that calls `each` or other Enumerable methods on the result works
658
+ unchanged; for `next`, call `each` first to get an Enumerator.
659
+ - Client and bidirectional streaming aren't supported yet. Stubs refuse
660
+ those methods with a `NotImplementedError`; keep the grpc gem for them
661
+ for now.
662
+
404
663
  ## Testing handlers
405
664
 
406
665
  `Tonic::Rails::Testing` calls a handler in-process, with real protobuf
@@ -634,6 +893,12 @@ When a call fails, `event.payload[:exception_object]` is the
634
893
  `Tonic::Rails::RpcError` sent to the client. If a handler raised something
635
894
  unexpected, that error's `cause` is the original exception.
636
895
 
896
+ Calls made with `Tonic::Rails::Client` publish `request_rpc.tonic_rails`,
897
+ with the same payload plus `:address`, the server called, and
898
+ `:attempts`, how many times it was tried. For a
899
+ server-streaming call, the event covers the whole stream, including the
900
+ time your block spends on each message.
901
+
637
902
  ## How it works
638
903
 
639
904
  ```
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "tonic_rails_native"
3
- version = "0.5.0"
3
+ version = "0.7.0"
4
4
  edition = "2024"
5
5
  rust-version = "1.88"
6
6
  publish = false
@@ -23,6 +23,10 @@ crate-type = ["cdylib"]
23
23
  #
24
24
  # socket2 configures the listener (SO_REUSEADDR/SO_REUSEPORT, backlog) and
25
25
  # adopts the listening socket worker processes inherit in multi-process mode.
26
+ #
27
+ # The client (`TonicRails::Native::Channel`) uses tonic's Channel with the
28
+ # same raw-byte passthrough. rustls-native-certs loads the system's root
29
+ # certificates for TLS, and base64 turns them into PEM for tonic.
26
30
  [dependencies]
27
31
  magnus = "0.9"
28
32
  rb-sys = "0.9"
@@ -45,3 +49,5 @@ http-body-util = "0.1"
45
49
  bytes = "1"
46
50
  tower = "0.5"
47
51
  flate2 = { version = "1", features = ["rust_backend"] }
52
+ rustls-native-certs = "0.8"
53
+ base64 = "0.22"
@@ -12,7 +12,6 @@ use std::task::{Context, Poll};
12
12
  use std::time::Instant;
13
13
 
14
14
  use bytes::Bytes;
15
- use http::HeaderMap;
16
15
  use http_body::{Body, Frame};
17
16
  use tokio::sync::mpsc;
18
17
  use tonic::{Code, Status};
@@ -71,35 +70,6 @@ impl ServerStats {
71
70
  }
72
71
  }
73
72
 
74
- /// Trailers-only error response for requests rejected before dispatch.
75
- pub struct ErrorBody {
76
- trailers: Option<HeaderMap>,
77
- }
78
-
79
- impl ErrorBody {
80
- pub fn new(code: Code, message: &str) -> Self {
81
- Self {
82
- trailers: Some(status_trailers(code as u32, message)),
83
- }
84
- }
85
- }
86
-
87
- impl Body for ErrorBody {
88
- type Data = Bytes;
89
- type Error = Status;
90
-
91
- fn poll_frame(
92
- mut self: Pin<&mut Self>,
93
- _cx: &mut Context<'_>,
94
- ) -> Poll<Option<Result<Frame<Bytes>, Status>>> {
95
- Poll::Ready(self.trailers.take().map(|t| Ok(Frame::trailers(t))))
96
- }
97
-
98
- fn is_end_stream(&self) -> bool {
99
- self.trailers.is_none()
100
- }
101
- }
102
-
103
73
  /// Streaming response body fed by the Ruby handler through an mpsc
104
74
  /// channel. Each `Data` message becomes a gRPC frame; `End` (or the
105
75
  /// deadline) produces the trailers.
@@ -232,6 +202,7 @@ impl Drop for ResponseBody {
232
202
  #[cfg(test)]
233
203
  mod tests {
234
204
  use super::*;
205
+ use http::HeaderMap;
235
206
  use std::task::Waker;
236
207
 
237
208
  type PolledFrame = Poll<Option<Result<Frame<Bytes>, Status>>>;
@@ -262,16 +233,6 @@ mod tests {
262
233
  }
263
234
  }
264
235
 
265
- #[test]
266
- fn error_body_yields_trailers_once() {
267
- let mut body = ErrorBody::new(Code::NotFound, "missing");
268
- let t = trailers(poll(&mut body));
269
- assert_eq!(t["grpc-status"], "5");
270
- assert_eq!(t["grpc-message"], "missing");
271
- assert!(matches!(poll(&mut body), Poll::Ready(None)));
272
- assert!(body.is_end_stream());
273
- }
274
-
275
236
  #[test]
276
237
  fn response_body_streams_data_then_ok() {
277
238
  let (tx, rx) = mpsc::channel(4);