Skip to content

Commit 76d4181

Browse files
authored
chore(deps)!: upgrade to hyper 1.x (#3504)
## ✨ chore(deps): upgrade to hyperium 1.x crates this branch performs an exciting upgrade for our proxy. this branch upgrades a number of our dependencies so that we use the 1.0 release family of the `hyper` http framework, and its ecosystem. see the [v1.0 announcement][hyper-v1] for more information. this branch upgrades the following dependencies: * `h2`: 0.3 -> 0.4 * `http`: 0.2 -> 1 * `http-body`: 0.4 -> 1 * `hyper`: 0.14.32 -> 1 * `prost`: 0.12 -> 0.13 * `prost-build`: 0.12 -> 0.13 * `prost-types`: 0.12 -> 0.13 * `tonic`: 0.10 -> 0.12 * `tonic-build`: 0.10 -> 0.12 a `hyper-util` dependency is added, which provides among other things, legacy-compatible interfaces such as `hyper_util::client::legacy::Client`, or glue to use `hyper` with the tokio runtime. see <https://docs.rs/hyper-util/latest/hyper_util/> for more information. a `http-body-util` dependency is added, which provides a `BodyExt` trait and a channel-backed body for use in unit tests. the `deprecated` feature flag that was active on our `0.14` hyper dependency has been removed, along with the `stream` and `runtime` feature flags. the `linkerd2-proxy-api` dependency is updated. see: <linkerd/linkerd2-proxy-api#421> ### 📝 notes for review bear particular attention to changes involving `http_body::Body` middleware. the change from two separate `poll_data()` and `poll_trailers()` functions, to a single `poll_frame()` method, induces some subtle changes to various pieces of middleware. also bear in mind that failing to set a timer, in our case `hyper_util::rt::TokioTimer`, can cause http/2 clients, or http/1 and http/2 servers, to panic. make sure that any uses of `hyper::server::conn::http1::Builder`, `hyper::client::conn::http1::Builder`, or `hyper::client::conn::http2::Builder` install a timer. ### ❗ breaking change: `l5d-proxy-error` values the `l5d-proxy-error` header can be examined to observe the cause of proxy errors encountered when sending meshed traffic. by virtue of this using a newer `hyper` client in the proxy, some error messages may in turn look different. for example, an error like `"connect timed out after 1s"` may now appear as `"client error (Connect)"`. ### 📚 other notes this work, by virtue of touching so many parts of the system, is carried out in distinct commits. an initial commit upgrades the dependencies at th workspace level. subsequent commits will not compile if the `--workspace` flag is provided, but the intent of this branch is to update each crate individually. use commands like, e.g. `cargo check --tests -p linkerd-proxy-http` to build particular crates at intermediate commits within this branch. this commit is also only the final leaf in an _extended_ line of work. this has been done to mitigate the effort of reviewing this change, and the risk of churn in the event of any unanticipated errors. see the top-level comment in linkerd/linkerd2#8733 for an overview of all of the work that brought us to this juncture. [hyper-v1]: https://seanmonstar.com/blog/hyper-v1/ --- * chore(deps): upgrade to hyper 1.x note: this commit will not compile, code changes are intentionally elided from this commit. this commit upgrades hyper, http, tonic, prost, related dependencies, and their assorted cargo features. see <linkerd/linkerd2#8733>. see also: * #3379 * #3380 * #3382 * #3405 * hyperium/hyper#3796 * #3411 * #3421 * #3427 * #3428 * #3432 * #3433 * #3444 * #3445 * #3454 * #3455 * #3456 * #3457 * #3461 * #3459 * #3465 * #3466 * #3467 * #3468 * linkerd/linkerd2-proxy-api#421 * linkerd/linkerd2#13492 * linkerd/linkerd2#13493 * hyperium/hyper#3816 * #3472 * #3473 * #3479 * tokio-rs/tokio#7059 * #3509 * hyperium/http-body#140 * #3515 * hyperium/http-body#141 * #3530 * #3531 * #3540 * #3556 * #3558 * #3559 * #3564 * #3567 * #3573 * #3583 * hyperium/http-body#144 * #3585 * #3586 * #3597 * #3598 * #3611 * #3614 * #3615 * #3616 * #3647 * #3651 * #3653 * #3654 * #3655 * #3656 * #3657 * #3660 * #3671 * #3672 * #3673 * #3676 * hyperium/http-body#147 * #3692 * #3699 * #3700 * #3701 * #3708 * linkerd/drain-rs#36 * #3715 * #3717 * eminence/procfs#340 --- squash: chore(deps): add hyper-util workspace dependency chore(deps): add http-body-util workspace dependency chore(deps): upgrade linkerd2-proxy-api this commit represents main as of linkerd/linkerd2-proxy-api#421. Signed-off-by: katelyn martin <kate@buoyant.io> * chore(http/box): upgrade to hyper 1.x Signed-off-by: katelyn martin <kate@buoyant.io> * chore(hyper-balance): upgrade to hyper 1.x Signed-off-by: katelyn martin <kate@buoyant.io> * chore(http/retain): ugrade to hyper 1.x Signed-off-by: katelyn martin <kate@buoyant.io> * chore(http/stream-timeouts): upgrade to hyper 1.x Signed-off-by: katelyn martin <kate@buoyant.io> * chore(http/classify): upgrade to hyper 1.x Signed-off-by: katelyn martin <kate@buoyant.io> * chore(http/upgrade): upgrade to hyper 1.x NOTE: there is a comment noting that the upgrade middleware does not expect to be cloneable. it is unfortunately, however, at odds with the new bounds expected of extensions. so, `Http11Upgrade` is now Clone'able, but a comment is left in place noting this weakened invariant. it's worth investigating how upgrades have changed since, in more detail, but for the current moment we are interested in being especially conservative about changing behavior, and focusing on api changes like `Body::poll_frame(..)`. Signed-off-by: katelyn martin <kate@buoyant.io> * chore(metrics): upgrade to hyper 1.x a brief note; this commit happened to tickle an unfortunate sharp edge in `BoxBody` and `Full`'s respective constructors. type inference could not figure out how to construct the body, so we refrain from boxing the response body now. Signed-off-by: katelyn martin <kate@buoyant.io> * chore(http/metrics): upgrade to hyper 1.x Signed-off-by: katelyn martin <kate@buoyant.io> * chore(http/prom): upgrade to hyper 1.x Signed-off-by: katelyn martin <kate@buoyant.io> * chore(http/insert): upgrade to hyper 1.x Signed-off-by: katelyn martin <kate@buoyant.io> * chore(http/retry): deprecate linkerd-http-body-compat Signed-off-by: katelyn martin <kate@buoyant.io> * chore(mock/http-body): upgrade to hyper 1.x Signed-off-by: katelyn martin <kate@buoyant.io> * chore(http/retry): upgrade to hyper 1.x Signed-off-by: katelyn martin <kate@buoyant.io> * chore(proxy/tap): upgrade to hyper 1.x Signed-off-by: katelyn martin <kate@buoyant.io> * chore(proxy/http): update to hyper 1.x Signed-off-by: katelyn martin <kate@buoyant.io> * chore(app/core): upgrade to hyper 1.x Signed-off-by: katelyn martin <kate@buoyant.io> * chore(app/test): upgrade to hyper 1.x Signed-off-by: katelyn martin <kate@buoyant.io> * chore(app/admin): upgrade to hyper 1.x Signed-off-by: katelyn martin <kate@buoyant.io> * chore(app/outbound): upgrade to hyper 1.x Signed-off-by: katelyn martin <kate@buoyant.io> * chore(app/inbound): upgrade to hyper 1.x Signed-off-by: katelyn martin <kate@buoyant.io> * chore(app/integration): upgrade to hyper 1.x Signed-off-by: katelyn martin <kate@buoyant.io> * chore(app): upgrade to hyper 1.x Signed-off-by: katelyn martin <kate@buoyant.io> * chore(transport-header): update generated code Signed-off-by: katelyn martin <kate@buoyant.io> * chore(spiffe-proto): update generated code Signed-off-by: katelyn martin <kate@buoyant.io> * chore(opencensus-proto): update generated code Signed-off-by: katelyn martin <kate@buoyant.io> * chore(opentelemetry-proto): update generated code Signed-off-by: katelyn martin <kate@buoyant.io> * chore(deny.toml): update cargo-deny directives this commit updates the contents of `deny.toml`. Signed-off-by: katelyn martin <kate@buoyant.io> * chore: `compile` has been renamed to `compile_protos` this addresses deprecation warnings, updating calls to a function that has since been renamed. Signed-off-by: katelyn martin <kate@buoyant.io> * chore(deps): remove `linkerd-http-body-compat` dependencies this commit removes this crate, which we added to future proof code for this upgrade, from its dependents. Signed-off-by: katelyn martin <kate@buoyant.io> * chore(http/body-compat): remove `linkerd-http-body-compat` crate Signed-off-by: katelyn martin <kate@buoyant.io> * chore(deps): update to drain 0.2.1 see linkerd/drain-rs#41. Signed-off-by: katelyn martin <kate@buoyant.io> --------- Signed-off-by: katelyn martin <kate@buoyant.io>
1 parent a41642a commit 76d4181

File tree

95 files changed

+1355
-1593
lines changed

Some content is hidden

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

95 files changed

+1355
-1593
lines changed

Cargo.lock

Lines changed: 223 additions & 123 deletions
Large diffs are not rendered by default.

Cargo.toml

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ members = [
2424
"linkerd/error-respond",
2525
"linkerd/exp-backoff",
2626
"linkerd/http/access-log",
27-
"linkerd/http/body-compat",
2827
"linkerd/http/box",
2928
"linkerd/http/classify",
3029
"linkerd/http/detect",
@@ -107,26 +106,31 @@ publish = false
107106

108107
[workspace.dependencies]
109108
bytes = { version = "1" }
110-
drain = { version = "0.1", default-features = false }
111-
h2 = { version = "0.3" }
112-
http = { version = "0.2" }
113-
http-body = { version = "0.4" }
114-
hyper = { version = "0.14.32", default-features = false }
109+
drain = { version = "0.2", default-features = false }
110+
h2 = { version = "0.4" }
111+
http = { version = "1" }
112+
http-body = { version = "1" }
113+
hyper = { version = "1", default-features = false }
114+
hyper-util = { version = "0.1", default-features = false }
115115
prometheus-client = { version = "0.23" }
116-
prost = { version = "0.12" }
117-
prost-build = { version = "0.12", default-features = false }
118-
prost-types = { version = "0.12" }
116+
prost = { version = "0.13" }
117+
prost-build = { version = "0.13", default-features = false }
118+
prost-types = { version = "0.13" }
119119
tokio-rustls = { version = "0.26", default-features = false, features = [
120120
"ring",
121121
"logging",
122122
] }
123-
tonic = { version = "0.10", default-features = false }
124-
tonic-build = { version = "0.10", default-features = false }
125-
tower = { version = "0.4.13", default-features = false }
123+
tonic = { version = "0.12", default-features = false }
124+
tonic-build = { version = "0.12", default-features = false }
125+
tower = { version = "0.4", default-features = false }
126126
tower-service = { version = "0.3" }
127127
tower-test = { version = "0.4" }
128128

129+
[workspace.dependencies.http-body-util]
130+
version = "0.1.3"
131+
default-features = false
132+
features = ["channel"]
133+
129134
[workspace.dependencies.linkerd2-proxy-api]
130-
version = "0.15.0"
131-
# git = "https://github.com/linkerd/linkerd2-proxy-api.git"
132-
# branch = "main"
135+
git = "https://github.com/linkerd/linkerd2-proxy-api"
136+
branch = "main"

deny.toml

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,11 @@ skip = [
5555
# `linkerd-trace-context`, `rustls-pemfile` and `tonic` depend on `base64`
5656
# v0.13.1 while `rcgen` depends on v0.21.5
5757
{ name = "base64" },
58-
# https://github.com/hawkw/matchers/pull/4
59-
{ name = "regex-automata", version = "0.1" },
60-
{ name = "regex-syntax", version = "0.6" },
61-
# Some dependencies still use indexmap v1.
62-
{ name = "indexmap", version = "1" },
63-
{ name = "hashbrown", version = "0.12" },
64-
58+
# itertools still uses itertools v0.13.
59+
{ name = "itertools", version = "0.13" },
60+
# tonic/axum depend on a newer `tower`, which we are still catching up to.
61+
# see #3744.
62+
{ name = "tower", version = "0.5" },
6563
]
6664
skip-tree = [
6765
# thiserror v2 is still propagating through the ecosystem
@@ -70,9 +68,14 @@ skip-tree = [
7068
{ name = "rand", version = "0.8" },
7169
# rust v1.0 is still propagating through the ecosystem
7270
{ name = "rustix", version = "0.38" },
71+
# `pprof` uses a number of old dependencies. for now, we skip its subtree.
72+
{ name = "pprof" }
7373
]
7474

7575
[sources]
7676
unknown-registry = "deny"
7777
unknown-git = "deny"
78-
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
78+
allow-registry = [
79+
"https://github.com/rust-lang/crates.io-index",
80+
"https://github.com/linkerd/linkerd2-proxy-api",
81+
]

hyper-balance/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ publish = { workspace = true }
1010
futures = { version = "0.3", default-features = false }
1111
http = { workspace = true }
1212
http-body = { workspace = true }
13-
hyper = { workspace = true, features = ["deprecated"] }
13+
hyper = { workspace = true }
1414
pin-project = "1"
1515
tower = { workspace = true, default-features = false, features = ["load"] }
1616
tokio = { version = "1", features = ["macros"] }

hyper-balance/src/lib.rs

Lines changed: 57 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -102,32 +102,20 @@ where
102102
self.body.is_end_stream()
103103
}
104104

105-
fn poll_data(
105+
fn poll_frame(
106106
self: Pin<&mut Self>,
107107
cx: &mut Context<'_>,
108-
) -> Poll<Option<Result<Self::Data, Self::Error>>> {
108+
) -> Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
109109
let this = self.project();
110-
let ret = futures::ready!(this.body.poll_data(cx));
110+
let ret = futures::ready!(this.body.poll_frame(cx));
111111

112-
// Once a data frame is received, the handle is dropped. On subsequent calls, this
112+
// Once a frame is received, the handle is dropped. On subsequent calls, this
113113
// is a noop.
114114
drop(this.handle.take());
115115

116116
Poll::Ready(ret)
117117
}
118118

119-
fn poll_trailers(
120-
self: Pin<&mut Self>,
121-
cx: &mut Context<'_>,
122-
) -> Poll<Result<Option<http::HeaderMap>, Self::Error>> {
123-
let this = self.project();
124-
// If this is being called, the handle definitely should have been dropped
125-
// already.
126-
drop(this.handle.take());
127-
128-
this.body.poll_trailers(cx)
129-
}
130-
131119
#[inline]
132120
fn size_hint(&self) -> hyper::body::SizeHint {
133121
self.body.size_hint()
@@ -157,35 +145,21 @@ impl<T: Send + 'static, B: Body> Body for PendingUntilEosBody<T, B> {
157145
self.body.is_end_stream()
158146
}
159147

160-
fn poll_data(
148+
fn poll_frame(
161149
self: Pin<&mut Self>,
162150
cx: &mut Context<'_>,
163-
) -> Poll<Option<Result<Self::Data, Self::Error>>> {
151+
) -> Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
164152
let mut this = self.project();
165153
let body = &mut this.body;
166154
tokio::pin!(body);
167-
let ret = futures::ready!(body.poll_data(cx));
155+
let frame = futures::ready!(body.poll_frame(cx));
168156

169157
// If this was the last frame, then drop the handle immediately.
170158
if this.body.is_end_stream() {
171159
drop(this.handle.take());
172160
}
173161

174-
Poll::Ready(ret)
175-
}
176-
177-
fn poll_trailers(
178-
self: Pin<&mut Self>,
179-
cx: &mut Context<'_>,
180-
) -> Poll<Result<Option<http::HeaderMap>, Self::Error>> {
181-
let this = self.project();
182-
let ret = futures::ready!(this.body.poll_trailers(cx));
183-
184-
// Once trailers are received, the handle is dropped immediately (in case the body
185-
// is retained longer for some reason).
186-
drop(this.handle.take());
187-
188-
Poll::Ready(ret)
162+
Poll::Ready(frame)
189163
}
190164

191165
#[inline]
@@ -198,7 +172,7 @@ impl<T: Send + 'static, B: Body> Body for PendingUntilEosBody<T, B> {
198172
mod tests {
199173
use super::{PendingUntilEos, PendingUntilFirstData};
200174
use futures::future::poll_fn;
201-
use http_body::Body;
175+
use http_body::{Body, Frame};
202176
use std::collections::VecDeque;
203177
use std::io::Cursor;
204178
use std::pin::Pin;
@@ -225,11 +199,13 @@ mod tests {
225199
assert_ready!(task::spawn(poll_fn(|cx| {
226200
let body = &mut body;
227201
tokio::pin!(body);
228-
body.poll_data(cx)
202+
body.poll_frame(cx)
229203
}))
230204
.poll())
231-
.expect("data some")
232-
.expect("data ok");
205+
.expect("frame is some")
206+
.expect("frame is ok")
207+
.into_data()
208+
.expect("frame is data");
233209
assert!(wk.upgrade().is_none());
234210
}
235211

@@ -282,10 +258,10 @@ mod tests {
282258
let res = assert_ready!(task::spawn(poll_fn(|cx| {
283259
let body = &mut body;
284260
tokio::pin!(body);
285-
body.poll_data(cx)
261+
body.poll_frame(cx)
286262
}))
287263
.poll());
288-
assert!(res.expect("data is some").is_err());
264+
assert!(res.expect("frame is some").is_err());
289265
assert!(wk.upgrade().is_none());
290266
}
291267

@@ -308,21 +284,21 @@ mod tests {
308284
assert_ready!(task::spawn(poll_fn(|cx| {
309285
let body = &mut body;
310286
tokio::pin!(body);
311-
body.poll_data(cx)
287+
body.poll_frame(cx)
312288
}))
313289
.poll())
314-
.expect("data some")
315-
.expect("data ok");
290+
.expect("frame is some")
291+
.expect("frame is ok");
316292
assert!(wk.upgrade().is_some());
317293

318294
assert_ready!(task::spawn(poll_fn(|cx| {
319295
let body = &mut body;
320296
tokio::pin!(body);
321-
body.poll_data(cx)
297+
body.poll_frame(cx)
322298
}))
323299
.poll())
324-
.expect("data some")
325-
.expect("data ok");
300+
.expect("frame is some")
301+
.expect("frame is ok");
326302
assert!(wk.upgrade().is_none());
327303
}
328304

@@ -355,40 +331,42 @@ mod tests {
355331
assert_ready!(task::spawn(poll_fn(|cx| {
356332
let body = &mut body;
357333
tokio::pin!(body);
358-
body.poll_data(cx)
334+
body.poll_frame(cx)
359335
}))
360336
.poll())
361-
.expect("data")
362-
.expect("data ok");
337+
.expect("frame is some")
338+
.expect("frame is ok");
363339
assert!(wk.upgrade().is_some());
364340

365341
assert_ready!(task::spawn(poll_fn(|cx| {
366342
let body = &mut body;
367343
tokio::pin!(body);
368-
body.poll_data(cx)
344+
body.poll_frame(cx)
369345
}))
370346
.poll())
371-
.expect("data")
372-
.expect("data ok");
347+
.expect("frame is some")
348+
.expect("frame is ok");
373349
assert!(wk.upgrade().is_some());
374350

375-
let poll = assert_ready!(task::spawn(poll_fn(|cx| {
351+
assert_ready!(task::spawn(poll_fn(|cx| {
376352
let body = &mut body;
377353
tokio::pin!(body);
378-
body.poll_data(cx)
354+
body.poll_frame(cx)
379355
}))
380-
.poll());
381-
assert!(poll.is_none());
382-
assert!(wk.upgrade().is_some());
356+
.poll())
357+
.expect("frame is some")
358+
.expect("frame is ok")
359+
.into_trailers()
360+
.expect("is trailers");
361+
assert!(wk.upgrade().is_none());
383362

384-
assert_ready!(task::spawn(poll_fn(|cx| {
363+
let poll = assert_ready!(task::spawn(poll_fn(|cx| {
385364
let body = &mut body;
386365
tokio::pin!(body);
387-
body.poll_trailers(cx)
366+
body.poll_frame(cx)
388367
}))
389-
.poll())
390-
.expect("trailers ok")
391-
.expect("trailers");
368+
.poll());
369+
assert!(poll.is_none());
392370
assert!(wk.upgrade().is_none());
393371
}
394372

@@ -411,7 +389,7 @@ mod tests {
411389
let poll = assert_ready!(task::spawn(poll_fn(|cx| {
412390
let body = &mut body;
413391
tokio::pin!(body);
414-
body.poll_data(cx)
392+
body.poll_frame(cx)
415393
}))
416394
.poll());
417395
assert!(poll.expect("some").is_err());
@@ -437,20 +415,21 @@ mod tests {
437415
self.0.is_empty() & self.1.is_none()
438416
}
439417

440-
fn poll_data(
418+
fn poll_frame(
441419
mut self: Pin<&mut Self>,
442420
_: &mut Context<'_>,
443-
) -> Poll<Option<Result<Self::Data, Self::Error>>> {
444-
Poll::Ready(self.as_mut().0.pop_front().map(Cursor::new).map(Ok))
445-
}
446-
447-
fn poll_trailers(
448-
mut self: Pin<&mut Self>,
449-
_: &mut Context<'_>,
450-
) -> Poll<Result<Option<http::HeaderMap>, Self::Error>> {
421+
) -> Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
451422
let mut this = self.as_mut();
452-
assert!(this.0.is_empty());
453-
Poll::Ready(Ok(this.1.take()))
423+
424+
// Return the next data frame from the sequence of chunks.
425+
if let Some(chunk) = this.0.pop_front() {
426+
let frame = Some(Ok(Frame::data(Cursor::new(chunk))));
427+
return Poll::Ready(frame);
428+
}
429+
430+
// Yield the trailers once all data frames have been yielded.
431+
let trailers = this.1.take().map(Frame::<Self::Data>::trailers).map(Ok);
432+
Poll::Ready(trailers)
454433
}
455434
}
456435

@@ -464,18 +443,13 @@ mod tests {
464443
self.0.is_none()
465444
}
466445

467-
fn poll_data(
446+
fn poll_frame(
468447
mut self: Pin<&mut Self>,
469448
_: &mut Context<'_>,
470-
) -> Poll<Option<Result<Self::Data, Self::Error>>> {
471-
Poll::Ready(Some(Err(self.as_mut().0.take().expect("err"))))
472-
}
449+
) -> Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
450+
let err = self.as_mut().0.take().expect("err");
473451

474-
fn poll_trailers(
475-
mut self: Pin<&mut Self>,
476-
_: &mut Context<'_>,
477-
) -> Poll<Result<Option<http::HeaderMap>, Self::Error>> {
478-
Poll::Ready(Err(self.as_mut().0.take().expect("err")))
452+
Poll::Ready(Some(Err(err)))
479453
}
480454
}
481455
}

linkerd/app/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pprof = ["linkerd-app-admin/pprof"]
1818

1919
[dependencies]
2020
futures = { version = "0.3", default-features = false }
21+
hyper-util = { workspace = true }
2122
linkerd-app-admin = { path = "./admin" }
2223
linkerd-app-core = { path = "./core" }
2324
linkerd-app-gateway = { path = "./gateway" }

linkerd/app/admin/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ bytes = { workspace = true }
1919
deflate = { version = "1", optional = true, features = ["gzip"] }
2020
http = { workspace = true }
2121
http-body = { workspace = true }
22-
hyper = { workspace = true, features = ["deprecated", "http1", "http2"] }
22+
http-body-util = { workspace = true }
23+
hyper = { workspace = true, features = ["http1", "http2"] }
2324
futures = { version = "0.3", default-features = false }
2425
pprof = { version = "0.14", optional = true, features = ["prost-codec"] }
2526
serde = "1"

0 commit comments

Comments
 (0)