Skip to content

Commit 9c1f2f9

Browse files
alexrudyikrivosheevallan2Ludea
authored
Upgrade to Hyper 1.0 & Axum 0.7 (#1670)
* Upgrade to hyper 1 and http 1 Upgrades only in Cargo.toml Co-authored-by: Ivan Krivosheev <[email protected]> Co-authored-by: Allan Zhang <[email protected]> * Convert from hyper::Body to http_body::BoxedBody When appropriate, we replace `hyper::Body` with `http_body::BoxedBody`, a good general purpose replacement for `hyper::Body`. Hyper does provide `hyper::body::Incoming`, but we cannot construct that, so anywhere we might need a body that we can construct (even most Service trait impls) we must use something like `http_body::BoxedBody`. When a service accepts `BoxedBody` and not `Incoming`, this indicates that the service is designed to run in places where it is not adjacent to hyper, for example, after routing (which is managed by Axum) Additionally, http >= 1 requires that extension types are `Clone`, so this bound has been added where appropriate. Co-authored-by: Ivan Krivosheev <[email protected]> Co-authored-by: Allan Zhang <[email protected]> * Convert tonic::codec::decode to use http >= 1 body types The Body trait has changed (removed `poll_data` and `poll_trailers`, they are now combined in `poll_frame`) and so the codec must be re-written to merge those two methods. Co-authored-by: Ivan Krivosheev <[email protected]> * Convert tonic::transport::channel to use http >= 1 body types tonic::transport::channel previously used `hyper::Body` as the response body type. This type no longer exists in hyper >= 1, and so has been converted to a `BoxBody` provided by `http_body_util` designed for interoperability between http crates. Co-authored-by: Ivan Krivosheev <[email protected]> Co-authored-by: Allan Zhang <[email protected]> * [tests] Convert tonic::codec::prost::tests to use http >= 1 body types The Body trait has changed (removed `poll_data` and `poll_trailers`, they are now combined in `poll_frame`) and so the codec must be re-written to merge those two methods. This also handles the return types which should now be wrapped in `Frame` when appropriate. Co-authored-by: Ivan Krivosheev <[email protected]> Co-authored-by: Allan Zhang <[email protected]> * Convert tonic::codec::encode to use http >= 1 body types The Body trait has changed (removed `poll_data` and `poll_trailers`, they are now combined in `poll_frame`) and so the codec must be re-written to merge those two methods. Co-authored-by: Ivan Krivosheev <[email protected]> Co-authored-by: Allan Zhang <[email protected]> * [tests] Convert tonic::service::interceptor::tests to use http >= 1 body types The Body trait has changed (removed `poll_data` and `poll_trailers`, they are now combined in `poll_frame`) and so the codec must be re-written to merge those two methods. This also handles the return types which should now be wrapped in `Frame` when appropriate. Co-authored-by: Ivan Krivosheev <[email protected]> Co-authored-by: Allan Zhang <[email protected]> * Convert tonic::transport to use http >= 1 body types Here, we must update some body types which are no longer valid. (A) BoxBody no longer has an `empty` method, instead we provide a helper in `tonic::body` for creating an empty boxed body via `http_body_util`. As well, `hyper::Body` is no longer a type, and instead, `hyper::Incoming` is used when directly recieving a Request from hyper, and `BoxBody` is used when the request may have passed through an axum router. In tonic, we prefer `BoxBody` as it allows for services to be used downstream from other components which enforce a specific body type (e.g. Axum), at the cost of making Body streaming opaque. Co-authored-by: Ivan Krivosheev <[email protected]> Co-authored-by: Allan Zhang <[email protected]> * Convert tonic::transport::server::recover_error to use http >= 1 body types The Body trait has changed (removed `poll_data` and `poll_trailers`, they are now combined in `poll_frame`) and so the codec must be re-written to merge those two methods. Co-authored-by: Ivan Krivosheev <[email protected]> Co-authored-by: Ludea <[email protected]> * Convert h2c examples to use http >= 1 body types In h2c, when a service is receiving from hyper, it has to accept a `hyper::body::Incoming` in hyper >= 1. Additionally, response bodies must be built from `http_body_util` combinators and become BoxBody objects. * [tests] Convert MergeTrailers body wrapper in interop server The Body trait has changed (removed `poll_data` and `poll_trailers`, they are now combined in `poll_frame`) and so the codec must be re-written to merge those two methods. * [tests] Convert compression tests to use hyper 1 body types The Body trait has changed (removed `poll_data` and `poll_trailers`, they are now combined in `poll_frame`) and so the codec must be re-written to merge those two methods. * [tests] Convert complex_tower_middleware Body for hyper 1 The Body trait has changed (removed `poll_data` and `poll_trailers`, they are now combined in `poll_frame`) and so the codec must be re-written to merge those two methods. * [tests] Convert integration_tests::origin to use http >= 1 body types The Body trait has changed (removed `poll_data` and `poll_trailers`, they are now combined in `poll_frame`) and so the codec must be re-written to merge those two methods. * Convert tonic-web to use http >= 1 body types The Body trait has changed (removed `poll_data` and `poll_trailers`, they are now combined in `poll_frame`) and so the codec must be re-written to merge those two methods. * Adapt for hyper-specific IO traits hyper >= 1 provides its own I/O traits (Read & Write) instead of relying on the equivalent traits from `tokio`. Then, `hyper-util` provides adaptor structs to wrap `tokio` I/O objects and implement the hyper equivalents. Therefore, we update the appropriate bounds to use the hyper traits, and update the I/O objects so that they are wrapped in the tokio to hyper adaptor. Co-authored-by: Ivan Krivosheev <[email protected]> Co-authored-by: Allan Zhang <[email protected]> * Upgrade axum to 0.7 Axum must be >= 0.7 to support hyper >= 1 Doing this also involves changing the Body type used. Since hyper >= 1 does not provide a generic body type, Axum and tonic both use `BoxBody` to provide a pointer to a Body. This changes the trait bounds required for methods which accept additional Serivces to be run alongside the primary GRPC service, since those will be routed with Axum, and therefore must accept a BoxBody. Co-authored-by: Ivan Krivosheev <[email protected]> Co-authored-by: Allan Zhang <[email protected]> * Convert service connector for hyper-1.0 Hyper >= 1 no longer includes automatic http2/http1 combined connections, and so we must swtich to the `http2::Builder` type (this is okay, we set http2_only(true) anyhow). As well, hyper >= 1 is generic over executors and does not directly depend on tokio. Since http2 connections can be multiplexed, they require some additional background task to handle sending and receiving requests. Additionally, these background tasks do not natively implement `tower::Service` since hyper >= 1 does not depend on `tower`. Therefore, we re-implement the `SendRequest` task as a tower::Service, so that it can be used within `Connection`, which expects to operate on a tower::Service to serve connections. Co-authored-by: Ivan Krivosheev <[email protected]> Co-authored-by: Allan Zhang <[email protected]> * Convert hyper::Client to hyper_util::legacy::Client `hyper::Client` has been moved to `hyper_util::legacy::Client` in version 1. Co-authored-by: Ivan Krivosheev <[email protected]> Co-authored-by: Allan Zhang <[email protected]> * Identify and propogate connect errors hyper::Error no longer provides information about Connect errors, especially since hyper_util now contains the connection implementation, it does not provide a separate error type. Instead, we create an internal Error type which is used in our own connectors, and then checked when figuring out what the gRPC status should be. * Remove hyper::server::conn::AddrStream hyper >= 1 has deprecated all of `hyper::server`, including `AddrStream` Co-authored-by: Ivan Krivosheev <[email protected]> Co-authored-by: Allan Zhang <[email protected]> Replace hyper::server::Accept hyper::server is deprectaed. Instead, we implement our own TCP-incoming based on the now removed hyper::server::Accept. In order to set `TCP_KEEPALIVE` we require the socket2 crate, since this option is not exposed in the standard library’s API. The implementaiton is inspired by that of hyper v0.14 * [examples] In h2c, replace hyper::Server with an accept loop hyper::Server is deprecated, with no current common replacement. Instead of implementing (or using tonic’s new) full server in here, we write a simple accept loop, which is sufficient to demonstrate the functionality of h2c. * Upgrade tls dependencies hyper-rustls requires version 0.27.0 to support hyper >= 1, bringing a few other tls bumps along. Importantly, we add the “ring” and “tls12” features to use ring as the crypto backend, consistent with previous versions of tonic. A future version of tonic might support selecting backends via features. Co-authored-by: Ivan Krivosheev <[email protected]> * Combine trailers when streaming decode body We aren't sure if multiple trailers should even be legal, but if we get multiple trailers in an HTTP body stream, we'll combine them all, to preserve their data. Alternatively we'd have to pick the first or last trailers, and that might lose information. * Tweak imports in transport example Example used `empty_body()`, which is now fully qualified as `tonic::body::empty_body()` to make clear that this is a tonic helper method for creating an empty BoxBody. * Remove commented out code from examples/h2c * tonic-web: avoid copy to vector to base64 encode * tonic-web: Merge subsequent trailer frames Ideally, a body should only return a single trailer frame. If multiple trailers are returned, merge them together. * Comment in tonic::status::find_status_in_source_chain Comment mentions why we choose “Unavailable” for connection errors * Make TowerToHyperService crate-private This also requires vendoring it in the rustls example, which doesn’t use a server type. Making the type crate-private means we can delete some unused methods. * Fixup imports in tonic::transport --------- Co-authored-by: Ivan Krivosheev <[email protected]> Co-authored-by: Allan Zhang <[email protected]> Co-authored-by: Ludea <[email protected]>
1 parent df85623 commit 9c1f2f9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+1104
-601
lines changed

examples/Cargo.toml

+16-13
Original file line numberDiff line numberDiff line change
@@ -271,21 +271,21 @@ routeguide = ["dep:async-stream", "tokio-stream", "dep:rand", "dep:serde", "dep:
271271
reflection = ["dep:tonic-reflection"]
272272
autoreload = ["tokio-stream/net", "dep:listenfd"]
273273
health = ["dep:tonic-health"]
274-
grpc-web = ["dep:tonic-web", "dep:bytes", "dep:http", "dep:hyper", "dep:tracing-subscriber", "dep:tower"]
274+
grpc-web = ["dep:tonic-web", "dep:bytes", "dep:http", "dep:hyper", "dep:hyper-util", "dep:tracing-subscriber", "dep:tower"]
275275
tracing = ["dep:tracing", "dep:tracing-subscriber"]
276-
uds = ["tokio-stream/net", "dep:tower", "dep:hyper"]
276+
uds = ["tokio-stream/net", "dep:tower", "dep:hyper", "dep:hyper-util"]
277277
streaming = ["tokio-stream", "dep:h2"]
278-
mock = ["tokio-stream", "dep:tower"]
279-
tower = ["dep:hyper", "dep:tower", "dep:http"]
278+
mock = ["tokio-stream", "dep:tower", "dep:hyper-util"]
279+
tower = ["dep:hyper", "dep:hyper-util", "dep:tower", "dep:http"]
280280
json-codec = ["dep:serde", "dep:serde_json", "dep:bytes"]
281281
compression = ["tonic/gzip"]
282282
tls = ["tonic/tls"]
283-
tls-rustls = ["dep:hyper", "dep:hyper-rustls", "dep:tower", "tower-http/util", "tower-http/add-extension", "dep:rustls-pemfile", "dep:tokio-rustls"]
283+
tls-rustls = ["dep:hyper", "dep:hyper-util", "dep:hyper-rustls", "dep:tower", "tower-http/util", "tower-http/add-extension", "dep:rustls-pemfile", "dep:tokio-rustls", "dep:pin-project", "dep:http-body-util"]
284284
dynamic-load-balance = ["dep:tower"]
285285
timeout = ["tokio/time", "dep:tower"]
286286
tls-client-auth = ["tonic/tls"]
287287
types = ["dep:tonic-types"]
288-
h2c = ["dep:hyper", "dep:tower", "dep:http"]
288+
h2c = ["dep:hyper", "dep:tower", "dep:http", "dep:hyper-util"]
289289
cancellation = ["dep:tokio-util"]
290290

291291
full = ["gcp", "routeguide", "reflection", "autoreload", "health", "grpc-web", "tracing", "uds", "streaming", "mock", "tower", "json-codec", "compression", "tls", "tls-rustls", "dynamic-load-balance", "timeout", "tls-client-auth", "types", "cancellation", "h2c"]
@@ -311,16 +311,19 @@ serde_json = { version = "1.0", optional = true }
311311
tracing = { version = "0.1.16", optional = true }
312312
tracing-subscriber = { version = "0.3", features = ["tracing-log", "fmt"], optional = true }
313313
prost-types = { version = "0.12", optional = true }
314-
http = { version = "0.2", optional = true }
315-
http-body = { version = "0.4.2", optional = true }
316-
hyper = { version = "0.14", optional = true }
314+
http = { version = "1", optional = true }
315+
http-body = { version = "1", optional = true }
316+
http-body-util = { version = "0.1", optional = true }
317+
hyper = { version = "1", optional = true }
318+
hyper-util = { version = ">=0.1.4, <0.2", optional = true }
317319
listenfd = { version = "1.0", optional = true }
318320
bytes = { version = "1", optional = true }
319321
h2 = { version = "0.3", optional = true }
320-
tokio-rustls = { version = "0.24.0", optional = true }
321-
hyper-rustls = { version = "0.24.0", features = ["http2"], optional = true }
322-
rustls-pemfile = { version = "1", optional = true }
323-
tower-http = { version = "0.4", optional = true }
322+
tokio-rustls = { version = "0.26", optional = true, features = ["ring", "tls12"], default-features = false }
323+
hyper-rustls = { version = "0.27.0", features = ["http2", "ring", "tls12"], optional = true, default-features = false }
324+
rustls-pemfile = { version = "2.0.0", optional = true }
325+
tower-http = { version = "0.5", optional = true }
326+
pin-project = { version = "1.0.11", optional = true }
324327

325328
[build-dependencies]
326329
tonic-build = { path = "../tonic-build", features = ["prost"] }

examples/src/grpc-web/client.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use hello_world::{greeter_client::GreeterClient, HelloRequest};
2+
use hyper_util::rt::TokioExecutor;
23
use tonic_web::GrpcWebClientLayer;
34

45
pub mod hello_world {
@@ -8,7 +9,7 @@ pub mod hello_world {
89
#[tokio::main]
910
async fn main() -> Result<(), Box<dyn std::error::Error>> {
1011
// Must use hyper directly...
11-
let client = hyper::Client::builder().build_http();
12+
let client = hyper_util::client::legacy::Client::builder(TokioExecutor::new()).build_http();
1213

1314
let svc = tower::ServiceBuilder::new()
1415
.layer(GrpcWebClientLayer::new())

examples/src/h2c/client.rs

+17-12
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use hello_world::greeter_client::GreeterClient;
22
use hello_world::HelloRequest;
33
use http::Uri;
4-
use hyper::Client;
4+
use hyper_util::client::legacy::Client;
5+
use hyper_util::rt::TokioExecutor;
56

67
pub mod hello_world {
78
tonic::include_proto!("helloworld");
@@ -11,7 +12,7 @@ pub mod hello_world {
1112
async fn main() -> Result<(), Box<dyn std::error::Error>> {
1213
let origin = Uri::from_static("http://[::1]:50051");
1314
let h2c_client = h2c::H2cChannel {
14-
client: Client::new(),
15+
client: Client::builder(TokioExecutor::new()).build_http(),
1516
};
1617

1718
let mut client = GreeterClient::with_origin(h2c_client, origin);
@@ -33,16 +34,20 @@ mod h2c {
3334
task::{Context, Poll},
3435
};
3536

36-
use hyper::{client::HttpConnector, Client};
37-
use tonic::body::BoxBody;
37+
use hyper::body::Incoming;
38+
use hyper_util::{
39+
client::legacy::{connect::HttpConnector, Client},
40+
rt::TokioExecutor,
41+
};
42+
use tonic::body::{empty_body, BoxBody};
3843
use tower::Service;
3944

4045
pub struct H2cChannel {
41-
pub client: Client<HttpConnector>,
46+
pub client: Client<HttpConnector, BoxBody>,
4247
}
4348

4449
impl Service<http::Request<BoxBody>> for H2cChannel {
45-
type Response = http::Response<hyper::Body>;
50+
type Response = http::Response<Incoming>;
4651
type Error = hyper::Error;
4752
type Future =
4853
Pin<Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send>>;
@@ -60,7 +65,7 @@ mod h2c {
6065
let h2c_req = hyper::Request::builder()
6166
.uri(origin)
6267
.header(http::header::UPGRADE, "h2c")
63-
.body(hyper::Body::empty())
68+
.body(empty_body())
6469
.unwrap();
6570

6671
let res = client.request(h2c_req).await.unwrap();
@@ -72,11 +77,11 @@ mod h2c {
7277
let upgraded_io = hyper::upgrade::on(res).await.unwrap();
7378

7479
// In an ideal world you would somehow cache this connection
75-
let (mut h2_client, conn) = hyper::client::conn::Builder::new()
76-
.http2_only(true)
77-
.handshake(upgraded_io)
78-
.await
79-
.unwrap();
80+
let (mut h2_client, conn) =
81+
hyper::client::conn::http2::Builder::new(TokioExecutor::new())
82+
.handshake(upgraded_io)
83+
.await
84+
.unwrap();
8085
tokio::spawn(conn);
8186

8287
h2_client.send_request(request).await

examples/src/h2c/server.rs

+36-18
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1+
use std::net::SocketAddr;
2+
3+
use hyper_util::rt::{TokioExecutor, TokioIo};
4+
use hyper_util::server::conn::auto::Builder;
5+
use hyper_util::service::TowerToHyperService;
6+
use tokio::net::TcpListener;
17
use tonic::{transport::Server, Request, Response, Status};
28

39
use hello_world::greeter_server::{Greeter, GreeterServer};
410
use hello_world::{HelloReply, HelloRequest};
5-
use tower::make::Shared;
611

712
pub mod hello_world {
813
tonic::include_proto!("helloworld");
@@ -28,28 +33,45 @@ impl Greeter for MyGreeter {
2833

2934
#[tokio::main]
3035
async fn main() -> Result<(), Box<dyn std::error::Error>> {
31-
let addr = "[::1]:50051".parse().unwrap();
36+
let addr: SocketAddr = "[::1]:50051".parse().unwrap();
3237
let greeter = MyGreeter::default();
3338

3439
println!("GreeterServer listening on {}", addr);
3540

41+
let incoming = TcpListener::bind(addr).await?;
3642
let svc = Server::builder()
3743
.add_service(GreeterServer::new(greeter))
3844
.into_router();
3945

4046
let h2c = h2c::H2c { s: svc };
4147

42-
let server = hyper::Server::bind(&addr).serve(Shared::new(h2c));
43-
server.await.unwrap();
44-
45-
Ok(())
48+
loop {
49+
match incoming.accept().await {
50+
Ok((io, _)) => {
51+
let router = h2c.clone();
52+
tokio::spawn(async move {
53+
let builder = Builder::new(TokioExecutor::new());
54+
let conn = builder.serve_connection_with_upgrades(
55+
TokioIo::new(io),
56+
TowerToHyperService::new(router),
57+
);
58+
let _ = conn.await;
59+
});
60+
}
61+
Err(e) => {
62+
eprintln!("Error accepting connection: {}", e);
63+
}
64+
}
65+
}
4666
}
4767

4868
mod h2c {
4969
use std::pin::Pin;
5070

5171
use http::{Request, Response};
52-
use hyper::Body;
72+
use hyper::body::Incoming;
73+
use hyper_util::{rt::TokioExecutor, service::TowerToHyperService};
74+
use tonic::{body::empty_body, transport::AxumBoxBody};
5375
use tower::Service;
5476

5577
#[derive(Clone)]
@@ -59,17 +81,14 @@ mod h2c {
5981

6082
type BoxError = Box<dyn std::error::Error + Send + Sync>;
6183

62-
impl<S> Service<Request<Body>> for H2c<S>
84+
impl<S> Service<Request<Incoming>> for H2c<S>
6385
where
64-
S: Service<Request<Body>, Response = Response<tonic::transport::AxumBoxBody>>
65-
+ Clone
66-
+ Send
67-
+ 'static,
86+
S: Service<Request<Incoming>, Response = Response<AxumBoxBody>> + Clone + Send + 'static,
6887
S::Future: Send + 'static,
6988
S::Error: Into<BoxError> + Sync + Send + 'static,
7089
S::Response: Send + 'static,
7190
{
72-
type Response = hyper::Response<Body>;
91+
type Response = hyper::Response<tonic::body::BoxBody>;
7392
type Error = hyper::Error;
7493
type Future =
7594
Pin<Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send>>;
@@ -81,20 +100,19 @@ mod h2c {
81100
std::task::Poll::Ready(Ok(()))
82101
}
83102

84-
fn call(&mut self, mut req: hyper::Request<Body>) -> Self::Future {
103+
fn call(&mut self, mut req: hyper::Request<Incoming>) -> Self::Future {
85104
let svc = self.s.clone();
86105
Box::pin(async move {
87106
tokio::spawn(async move {
88107
let upgraded_io = hyper::upgrade::on(&mut req).await.unwrap();
89108

90-
hyper::server::conn::Http::new()
91-
.http2_only(true)
92-
.serve_connection(upgraded_io, svc)
109+
hyper::server::conn::http2::Builder::new(TokioExecutor::new())
110+
.serve_connection(upgraded_io, TowerToHyperService::new(svc))
93111
.await
94112
.unwrap();
95113
});
96114

97-
let mut res = hyper::Response::new(hyper::Body::empty());
115+
let mut res = hyper::Response::new(empty_body());
98116
*res.status_mut() = http::StatusCode::SWITCHING_PROTOCOLS;
99117
res.headers_mut().insert(
100118
hyper::header::UPGRADE,

examples/src/interceptor/server.rs

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ fn intercept(mut req: Request<()>) -> Result<Request<()>, Status> {
5757
Ok(req)
5858
}
5959

60+
#[derive(Clone)]
6061
struct MyExtension {
6162
some_piece_of_data: String,
6263
}

examples/src/mock/mock.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use hyper_util::rt::TokioIo;
12
use tonic::{
23
transport::{Endpoint, Server, Uri},
34
Request, Response, Status,
@@ -36,7 +37,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
3637

3738
async move {
3839
if let Some(client) = client {
39-
Ok(client)
40+
Ok(TokioIo::new(client))
4041
} else {
4142
Err(std::io::Error::new(
4243
std::io::ErrorKind::Other,

examples/src/tls_rustls/client.rs

+5-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ pub mod pb {
55
tonic::include_proto!("/grpc.examples.unaryecho");
66
}
77

8-
use hyper::{client::HttpConnector, Uri};
8+
use hyper::Uri;
9+
use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor};
910
use pb::{echo_client::EchoClient, EchoRequest};
1011
use tokio_rustls::rustls::{ClientConfig, RootCertStore};
1112

@@ -17,11 +18,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
1718
let mut roots = RootCertStore::empty();
1819

1920
let mut buf = std::io::BufReader::new(&fd);
20-
let certs = rustls_pemfile::certs(&mut buf)?;
21-
roots.add_parsable_certificates(&certs);
21+
let certs = rustls_pemfile::certs(&mut buf).collect::<Result<Vec<_>, _>>()?;
22+
roots.add_parsable_certificates(certs.into_iter());
2223

2324
let tls = ClientConfig::builder()
24-
.with_safe_defaults()
2525
.with_root_certificates(roots)
2626
.with_no_client_auth();
2727

@@ -47,7 +47,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
4747
.map_request(|_| Uri::from_static("https://[::1]:50051"))
4848
.service(http);
4949

50-
let client = hyper::Client::builder().build(connector);
50+
let client = hyper_util::client::legacy::Client::builder(TokioExecutor::new()).build(connector);
5151

5252
// Using `with_origin` will let the codegenerated client set the `scheme` and
5353
// `authority` from the porvided `Uri`.

0 commit comments

Comments
 (0)