Skip to content

Commit 34f6a10

Browse files
authored
chore(volo-http): add TimeoutLayer and FailOnStatus layer (#522)
Since the previous `MetaService` was too coupled with the client configuration, it was very inelegant. So we removed `MetaService` and used `TimeoutLayer` and `FailOnStatus` layers to replace the previous `MetaService` with the two configs. BREAK CHANGES: - `ClientBuilder::fail_on_status` has been removed, users should use `FailOnStatus` layer instead - `ClientBuilder::set_request_timeout` has been removed, users should use `TimeoutLayer` instead TODO: - Refactor `Target`, `CallOpt` and `Config` - Support setting timeout at request level Signed-off-by: Yu Li <liyu.yukiteru@bytedance.com>
1 parent ff7c7af commit 34f6a10

File tree

9 files changed

+261
-221
lines changed

9 files changed

+261
-221
lines changed

examples/src/http/example-http-client.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,7 @@ async fn main() -> Result<(), BoxError> {
4444
.callee_name("example.http.server")
4545
// set default target address
4646
.address("127.0.0.1:8080".parse::<SocketAddr>().unwrap())
47-
.header("Test", "Test")?
48-
.fail_on_error_status(true);
47+
.header("Test", "Test")?;
4948
builder.build()
5049
};
5150

volo-http/src/client/layer.rs

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
//! Collections of some useful `Layer`s.
2+
3+
use std::{error::Error, fmt, time::Duration};
4+
5+
use http::status::StatusCode;
6+
use motore::{layer::Layer, service::Service};
7+
8+
use crate::{
9+
error::{client::request_error, ClientError},
10+
response::ClientResponse,
11+
};
12+
13+
/// [`Layer`] for setting timeout to the request.
14+
///
15+
/// See [`TimeoutLayer::new`] for more details.
16+
pub struct TimeoutLayer {
17+
duration: Duration,
18+
}
19+
20+
impl TimeoutLayer {
21+
/// Create a new [`TimeoutLayer`] with given [`Duration`].
22+
///
23+
/// If the request times out, an error [`Timeout`] is returned.
24+
///
25+
/// [`Timeout`]: crate::error::client::Timeout
26+
pub fn new(duration: Duration) -> Self {
27+
Self { duration }
28+
}
29+
}
30+
31+
impl<S> Layer<S> for TimeoutLayer {
32+
type Service = TimeoutService<S>;
33+
34+
fn layer(self, inner: S) -> Self::Service {
35+
TimeoutService {
36+
inner,
37+
duration: self.duration,
38+
}
39+
}
40+
}
41+
42+
/// The [`Service`] generated by [`TimeoutLayer`].
43+
///
44+
/// See [`TimeoutLayer`] and [`TimeoutLayer::new`] for more details.
45+
pub struct TimeoutService<S> {
46+
inner: S,
47+
duration: Duration,
48+
}
49+
50+
impl<Cx, Req, S> Service<Cx, Req> for TimeoutService<S>
51+
where
52+
Cx: Send,
53+
Req: Send,
54+
S: Service<Cx, Req, Error = ClientError> + Send + Sync,
55+
{
56+
type Response = S::Response;
57+
type Error = S::Error;
58+
59+
async fn call(&self, cx: &mut Cx, req: Req) -> Result<Self::Response, Self::Error> {
60+
let fut = self.inner.call(cx, req);
61+
let sleep = tokio::time::sleep(self.duration);
62+
63+
tokio::select! {
64+
res = fut => res,
65+
_ = sleep => {
66+
tracing::error!("[Volo-HTTP] request timeout");
67+
Err(crate::error::client::timeout())
68+
}
69+
}
70+
}
71+
}
72+
73+
/// [`Layer`] for throwing service error with the response's error status code.
74+
///
75+
/// Users can use [`FailOnStatus::all`], [`FailOnStatus::client_error`] or
76+
/// [`FailOnStatus::server_error`] for creating the [`FailOnStatus`] layer that convert all (4XX and
77+
/// 5XX), client error (4XX) or server error (5XX) to a error of service.
78+
pub struct FailOnStatus {
79+
client_error: bool,
80+
server_error: bool,
81+
}
82+
83+
impl FailOnStatus {
84+
/// Create a [`FailOnStatus`] layer that return error [`StatusCodeError`] for all error status
85+
/// codes (4XX and 5XX).
86+
pub fn all() -> Self {
87+
Self {
88+
client_error: true,
89+
server_error: true,
90+
}
91+
}
92+
93+
/// Create a [`FailOnStatus`] layer that return error [`StatusCodeError`] for client error
94+
/// status codes (4XX).
95+
pub fn client_error() -> Self {
96+
Self {
97+
client_error: true,
98+
server_error: false,
99+
}
100+
}
101+
102+
/// Create a [`FailOnStatus`] layer that return error [`StatusCodeError`] for server error
103+
/// status codes (5XX).
104+
pub fn server_error() -> Self {
105+
Self {
106+
client_error: false,
107+
server_error: true,
108+
}
109+
}
110+
}
111+
112+
impl<S> Layer<S> for FailOnStatus {
113+
type Service = FailOnStatusService<S>;
114+
115+
fn layer(self, inner: S) -> Self::Service {
116+
FailOnStatusService {
117+
inner,
118+
fail_on: self,
119+
}
120+
}
121+
}
122+
123+
/// The [`Service`] generated by [`FailOnStatus`] layer.
124+
///
125+
/// See [`FailOnStatus`] for more details.
126+
pub struct FailOnStatusService<S> {
127+
inner: S,
128+
fail_on: FailOnStatus,
129+
}
130+
131+
impl<Cx, Req, S, B> Service<Cx, Req> for FailOnStatusService<S>
132+
where
133+
Cx: Send,
134+
Req: Send,
135+
S: Service<Cx, Req, Response = ClientResponse<B>, Error = ClientError> + Send + Sync,
136+
{
137+
type Response = S::Response;
138+
type Error = S::Error;
139+
140+
async fn call(&self, cx: &mut Cx, req: Req) -> Result<Self::Response, Self::Error> {
141+
let resp = self.inner.call(cx, req).await?;
142+
let status = resp.status();
143+
if (self.fail_on.client_error && status.is_client_error())
144+
|| (self.fail_on.server_error && status.is_server_error())
145+
{
146+
Err(request_error(StatusCodeError::new(status)))
147+
} else {
148+
Ok(resp)
149+
}
150+
}
151+
}
152+
153+
/// Client received a response with an error status code.
154+
pub struct StatusCodeError {
155+
/// The original status code
156+
pub status: StatusCode,
157+
}
158+
159+
impl StatusCodeError {
160+
fn new(status: StatusCode) -> Self {
161+
Self { status }
162+
}
163+
}
164+
165+
impl fmt::Debug for StatusCodeError {
166+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167+
f.debug_struct("StatusCodeError")
168+
.field("status", &self.status)
169+
.finish()
170+
}
171+
}
172+
173+
impl fmt::Display for StatusCodeError {
174+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175+
write!(f, "client received an error status code: {}", self.status)
176+
}
177+
}
178+
179+
impl Error for StatusCodeError {}
180+
181+
#[cfg(test)]
182+
mod client_layers_tests {
183+
use http::status::StatusCode;
184+
use motore::service::Service;
185+
186+
use super::FailOnStatus;
187+
use crate::{
188+
body::Body, client::test_helpers::MockTransport, context::ClientContext,
189+
error::ClientError, request::ClientRequest, response::ClientResponse, ClientBuilder,
190+
};
191+
192+
struct ReturnStatus;
193+
194+
impl Service<ClientContext, ClientRequest> for ReturnStatus {
195+
type Response = ClientResponse;
196+
type Error = ClientError;
197+
198+
fn call(
199+
&self,
200+
_: &mut ClientContext,
201+
req: ClientRequest,
202+
) -> impl std::future::Future<Output = Result<Self::Response, Self::Error>> + Send {
203+
let path = req.uri().path();
204+
assert_eq!(&path[..1], "/");
205+
let status_code = path[1..].parse::<u16>().expect("invalid uri");
206+
let status_code = StatusCode::from_u16(status_code).expect("invalid status code");
207+
let mut resp = ClientResponse::new(Body::empty());
208+
*resp.status_mut() = status_code;
209+
async { Ok(resp) }
210+
}
211+
}
212+
213+
#[tokio::test]
214+
async fn fail_on_status_test() {
215+
{
216+
// Reject all error status codes
217+
let client = ClientBuilder::new()
218+
.layer_outer_front(FailOnStatus::all())
219+
.mock(MockTransport::service(ReturnStatus));
220+
client.get("/400").send().await.unwrap_err();
221+
client.get("/500").send().await.unwrap_err();
222+
}
223+
{
224+
// Reject client error status codes
225+
let client = ClientBuilder::new()
226+
.layer_outer_front(FailOnStatus::client_error())
227+
.mock(MockTransport::service(ReturnStatus));
228+
client.get("/400").send().await.unwrap_err();
229+
// 5XX is server error, it should not be handled
230+
client.get("/500").send().await.unwrap();
231+
}
232+
{
233+
// Reject all error status codes
234+
let client = ClientBuilder::new()
235+
.layer_outer_front(FailOnStatus::server_error())
236+
.mock(MockTransport::service(ReturnStatus));
237+
// 4XX is client error, it should not be handled
238+
client.get("/400").send().await.unwrap();
239+
client.get("/500").send().await.unwrap_err();
240+
}
241+
}
242+
}

volo-http/src/client/meta.rs

Lines changed: 0 additions & 72 deletions
This file was deleted.

0 commit comments

Comments
 (0)