Skip to content

Commit 9f52ccb

Browse files
authored
Global Error Handler and hot-path optimizations (#163)
* replaced `Parts` cloning on the request hot path with arguments extraction, similar to route's error handler * comment fix * PathArgs tweaks
1 parent 73d8fbc commit 9f52ccb

File tree

7 files changed

+136
-70
lines changed

7 files changed

+136
-70
lines changed

volga/src/app/scope.rs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ use hyper::{
1414
};
1515

1616
use crate::{
17-
app::AppEnv,
18-
error::{Error, handler::call_weak_err_handler},
17+
app::AppEnv,
18+
error::{Error, handler::extract_error_args},
1919
headers::CACHE_CONTROL,
2020
http::endpoints::FindResult,
2121
HttpRequest, HttpBody, HttpResult, ClientIp,
@@ -194,7 +194,7 @@ async fn handle_impl(
194194

195195
let error_handler = pipeline.error_handler();
196196
let (mut parts, body) = request.into_parts();
197-
197+
198198
{
199199
let extensions = &mut parts.extensions;
200200
extensions.insert(ClientIp(peer_addr));
@@ -204,7 +204,7 @@ async fn handle_impl(
204204

205205
#[cfg(feature = "ws")]
206206
extensions.insert(error_handler.clone());
207-
207+
208208
#[cfg(feature = "jwt-auth")]
209209
if let Some(bts) = &env.bearer_token_service {
210210
extensions.insert(bts.clone());
@@ -225,16 +225,19 @@ async fn handle_impl(
225225
if let Some(rate_limiter) = &env.rate_limiter {
226226
extensions.insert(rate_limiter.clone());
227227
}
228-
228+
229229
if let Some(trusted_proxies) = &env.trusted_proxies {
230230
extensions.insert(TrustedProxies(trusted_proxies.clone()));
231231
}
232232
}
233233
}
234234

235-
let request = HttpRequest::new(Request::from_parts(parts.clone(), body))
235+
// Pre-extract error handler args from parts before consuming them.
236+
let error_args = extract_error_args(&error_handler, &parts);
237+
238+
let request = HttpRequest::new(Request::from_parts(parts, body))
236239
.into_limited(env.body_limit);
237-
240+
238241
#[cfg(feature = "middleware")]
239242
let response = if pipeline.has_middleware_pipeline() {
240243
let ctx = HttpContext::new(request, Some(route_pipeline), cors);
@@ -244,10 +247,13 @@ async fn handle_impl(
244247
};
245248
#[cfg(not(feature = "middleware"))]
246249
let response = route_pipeline.call(request).await;
247-
250+
248251
match response {
249252
Ok(response) => Ok(response),
250-
Err(err) => call_weak_err_handler(error_handler, parts, err).await,
253+
Err(err) => match error_args {
254+
Some(args) => args.call(err).await,
255+
None => Err(Error::server_error("Server Error: error handler could not be upgraded")),
256+
},
251257
}
252258
}
253259
}
@@ -323,7 +329,6 @@ fn apply_hsts_headers(
323329
host: Option<HeaderValue>,
324330
) {
325331
if is_excluded(host, &hsts.exclude_hosts) {
326-
println!("22");
327332
return;
328333
}
329334

volga/src/error/handler.rs

Lines changed: 99 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! Error Handler
1+
//! Error Handler
22
33
use futures_util::future::BoxFuture;
44
use hyper::http::request::Parts;
@@ -7,10 +7,29 @@ use super::Error;
77

88
use std::sync::{Arc, Weak};
99

10-
/// Trait for types that represents an error handler
11-
pub trait ErrorHandler {
12-
/// Calls the error handler function
13-
fn call(&self, parts: &Parts, err: Error) -> BoxFuture<'_, HttpResult>;
10+
/// Trait for types that represent a global error handler.
11+
///
12+
/// Instead of receiving full request [`Parts`] at error time (which would
13+
/// require cloning them on every request), an `ErrorHandler` pre-extracts
14+
/// whatever arguments it needs *before* the parts are consumed and returns
15+
/// them as a type-erased [`ErasedErrorArgs`]. The extracted args are dropped
16+
/// on the happy path and invoked only if the handler returns an error.
17+
pub trait ErrorHandler: Send + Sync {
18+
/// Extracts handler arguments from the request parts before they are consumed.
19+
///
20+
/// Called on every matched request. The returned [`ErasedErrorArgs`] is
21+
/// dropped if no error occurs, or invoked with the error if one does.
22+
fn extract(&self, parts: &Parts) -> Box<dyn ErasedErrorArgs + Send>;
23+
}
24+
25+
/// Type-erased, pre-extracted error handler arguments.
26+
///
27+
/// Produced by [`ErrorHandler::extract`] before request parts are consumed.
28+
/// Stores the handler closure and its already-extracted arguments so that
29+
/// `parts.clone()` is never needed on the hot path.
30+
pub trait ErasedErrorArgs: Send {
31+
/// Invokes the error handler with the given error.
32+
fn call(self: Box<Self>, err: Error) -> BoxFuture<'static, HttpResult>;
1433
}
1534

1635
/// Owns a closure that handles an error
@@ -22,7 +41,7 @@ where
2241
Args: FromRequestParts + Send
2342
{
2443
func: F,
25-
_marker: std::marker::PhantomData<fn(Args)>,
44+
_marker: std::marker::PhantomData<fn(Args) -> R>,
2645
}
2746

2847
impl<F, R, Args> ErrorFunc<F, R, Args>
@@ -39,26 +58,70 @@ where
3958
}
4059
}
4160

42-
impl<F, R, Args> ErrorHandler for ErrorFunc<F, R, Args>
61+
/// Stores a pre-extracted handler invocation: the function, its arguments,
62+
/// and the request URI (for `err.instance`). Allocated once per request
63+
/// instead of cloning the full `Parts`.
64+
struct BoundErrorArgs<F, Args> {
65+
func: F,
66+
args: Args,
67+
uri: String,
68+
}
69+
70+
impl<F, Args> ErasedErrorArgs for BoundErrorArgs<F, Args>
4371
where
44-
F: MapErrHandler<Args, Output = R>,
45-
R: IntoResponse + 'static,
46-
Args: FromRequestParts + Send + 'static,
72+
F: MapErrHandler<Args> + Send + 'static,
73+
F::Output: IntoResponse + 'static,
74+
Args: Send + 'static,
4775
{
48-
#[inline]
49-
fn call(&self, parts: &Parts, err: Error) -> BoxFuture<'_, HttpResult> {
50-
let Ok(args) = Args::from_parts(parts) else {
51-
return Box::pin(default_error_handler(err));
52-
};
76+
fn call(self: Box<Self>, mut err: Error) -> BoxFuture<'static, HttpResult> {
5377
Box::pin(async move {
54-
match self.func.call(err, args).await.into_response() {
78+
if err.instance.is_none() {
79+
err.instance = Some(self.uri);
80+
}
81+
match self.func.call(err, self.args).await.into_response() {
5582
Ok(resp) => Ok(resp),
5683
Err(err) => default_error_handler(err).await,
5784
}
5885
})
5986
}
6087
}
6188

89+
/// Fallback used when `Args` extraction fails; delegates to the default handler.
90+
struct DefaultErrorArgs {
91+
uri: String,
92+
}
93+
94+
impl ErasedErrorArgs for DefaultErrorArgs {
95+
fn call(self: Box<Self>, mut err: Error) -> BoxFuture<'static, HttpResult> {
96+
Box::pin(async move {
97+
if err.instance.is_none() {
98+
err.instance = Some(self.uri);
99+
}
100+
default_error_handler(err).await
101+
})
102+
}
103+
}
104+
105+
impl<F, R, Args> ErrorHandler for ErrorFunc<F, R, Args>
106+
where
107+
F: MapErrHandler<Args, Output = R> + Clone + 'static,
108+
R: IntoResponse + 'static,
109+
Args: FromRequestParts + Send + 'static,
110+
{
111+
#[inline]
112+
fn extract(&self, parts: &Parts) -> Box<dyn ErasedErrorArgs + Send> {
113+
let uri = parts.uri.to_string();
114+
match Args::from_parts(parts) {
115+
Ok(args) => Box::new(BoundErrorArgs {
116+
func: self.func.clone(),
117+
args,
118+
uri,
119+
}),
120+
Err(_) => Box::new(DefaultErrorArgs { uri }),
121+
}
122+
}
123+
}
124+
62125
impl<F, R, Args> From<ErrorFunc<F, R, Args>> for PipelineErrorHandler
63126
where
64127
F: MapErrHandler<Args, Output = R>,
@@ -74,14 +137,14 @@ where
74137
/// Holds a reference to global error handler
75138
pub(crate) type PipelineErrorHandler = Arc<
76139
dyn ErrorHandler
77-
+ Send
140+
+ Send
78141
+ Sync
79142
>;
80143

81144
/// Weak version of [`crate::error::PipelineErrorHandler`]
82145
pub(crate) type WeakErrorHandler = Weak<
83146
dyn ErrorHandler
84-
+ Send
147+
+ Send
85148
+ Sync
86149
>;
87150

@@ -91,16 +154,16 @@ pub(crate) async fn default_error_handler(err: Error) -> HttpResult {
91154
status!(err.status.as_u16(), "{err}")
92155
}
93156

157+
/// Extracts error handler arguments from request parts before they are consumed.
158+
///
159+
/// Returns `None` if the handler `Weak` reference can no longer be upgraded
160+
/// (i.e. the server is shutting down).
94161
#[inline]
95-
pub(crate) async fn call_weak_err_handler(error_handler: WeakErrorHandler, parts: Parts, mut err: Error) -> HttpResult {
96-
if err.instance.is_none() {
97-
err.instance = Some(parts.uri.to_string());
98-
}
99-
error_handler
100-
.upgrade()
101-
.ok_or(Error::server_error("Server Error: error handler could not be upgraded"))?
102-
.call(&parts, err)
103-
.await
162+
pub(crate) fn extract_error_args(
163+
handler: &WeakErrorHandler,
164+
parts: &Parts,
165+
) -> Option<Box<dyn ErasedErrorArgs + Send>> {
166+
handler.upgrade().map(|h| h.extract(parts))
104167
}
105168

106169
#[cfg(test)]
@@ -114,8 +177,8 @@ mod tests {
114177
ErrorFunc,
115178
PipelineErrorHandler,
116179
WeakErrorHandler,
117-
default_error_handler,
118-
call_weak_err_handler
180+
default_error_handler,
181+
extract_error_args,
119182
};
120183

121184
#[tokio::test]
@@ -126,7 +189,7 @@ mod tests {
126189

127190
let mut response = response.unwrap();
128191
let body = &response.body_mut().collect().await.unwrap().to_bytes();
129-
192+
130193
assert_eq!(response.status(), 500);
131194
assert_eq!(String::from_utf8_lossy(body), "Some error");
132195
}
@@ -153,8 +216,9 @@ mod tests {
153216

154217
let req = Request::get("/foo/bar?baz").body(()).unwrap();
155218
let (parts, _) = req.into_parts();
156-
let response = handler.call(&parts, error).await;
157-
219+
let extracted = handler.extract(&parts);
220+
let response = extracted.call(error).await;
221+
158222
assert!(response.is_ok());
159223

160224
let mut response = response.unwrap();
@@ -175,7 +239,8 @@ mod tests {
175239

176240
let req = Request::get("/foo/bar?baz").body(()).unwrap();
177241
let (parts, _) = req.into_parts();
178-
let response = call_weak_err_handler(weak_handler, parts, error).await;
242+
let extracted = extract_error_args(&weak_handler, &parts).expect("handler alive");
243+
let response = extracted.call(error).await;
179244
assert!(response.is_ok());
180245

181246
let mut response = response.unwrap();
@@ -184,4 +249,4 @@ mod tests {
184249
assert_eq!(response.status(), 403);
185250
assert_eq!(body.len(), 0);
186251
}
187-
}
252+
}

volga/src/http/endpoints/args/path.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ impl FromPayload for String {
284284
#[inline]
285285
fn from_payload(payload: Payload<'_>) -> Self::Future {
286286
let Payload::Path(param) = payload else { unreachable!() };
287-
ok(param.value.as_ref().to_owned())
287+
ok(param.value.into_string())
288288
}
289289
}
290290

@@ -303,7 +303,7 @@ impl FromPayload for Cow<'static, str> {
303303
#[inline]
304304
fn from_payload(payload: Payload<'_>) -> Self::Future {
305305
let Payload::Path(param) = payload else { unreachable!() };
306-
ok(Cow::Owned(param.value.as_ref().to_owned()))
306+
ok(Cow::Owned(param.value.into_string()))
307307
}
308308
}
309309

@@ -322,14 +322,14 @@ impl FromPayload for Box<str> {
322322
#[inline]
323323
fn from_payload(payload: Payload<'_>) -> Self::Future {
324324
let Payload::Path(param) = payload else { unreachable!() };
325-
ok(param.value.as_ref().into())
325+
ok(param.value)
326326
}
327327
}
328328

329329
impl FromPathArg for Box<str> {
330330
#[inline]
331331
fn from_path_arg(arg: &PathArg) -> Result<Self, Error> {
332-
Ok(arg.value.as_ref().into())
332+
Ok(arg.value.clone())
333333
}
334334
}
335335

@@ -341,7 +341,7 @@ impl FromPayload for Box<[u8]> {
341341
#[inline]
342342
fn from_payload(payload: Payload<'_>) -> Self::Future {
343343
let Payload::Path(param) = payload else { unreachable!() };
344-
ok(param.value.as_bytes().into())
344+
ok(param.value.into_boxed_bytes())
345345
}
346346
}
347347

volga/src/http/endpoints/route.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ impl RouteNode {
199199
if let Some(next) = &current.dynamic_route {
200200
params.push(PathArg {
201201
name: Arc::clone(&next.path),
202-
value: Arc::from(segment)
202+
value: Box::from(segment)
203203
});
204204
current = next.node.as_ref();
205205
continue;

volga/src/http/endpoints/route/path_args.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ pub struct PathArg {
3131
pub(crate) name: Arc<str>,
3232

3333
/// Argument value
34-
pub(crate) value: Arc<str>,
34+
pub(crate) value: Box<str>,
3535
}
3636

3737
impl PathArgs {

volga/src/tls.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -606,7 +606,7 @@ impl App {
606606
self
607607
}
608608

609-
/// Configures web server with specified TLS configuration
609+
/// Configures a web server with a specified TLS configuration
610610
///
611611
/// Default: `None`
612612
pub fn set_tls(mut self, config: TlsConfig) -> Self {

0 commit comments

Comments
 (0)