Skip to content

Commit 2aeb9d5

Browse files
authored
Middleware perf tuning (#167)
* middleware performance tuning + security & safety docs * zero-alloc state machine for next fn * updated CHANGELOG.md
1 parent 2872d32 commit 2aeb9d5

File tree

4 files changed

+134
-30
lines changed

4 files changed

+134
-30
lines changed

CHANGELOG.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,18 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/).
77

8-
## Unreleased
8+
## 0.8.6
9+
10+
### Added
11+
* fuzz tests for router and OpenAPI (#166)
912

1013
## Changed
11-
* Refactored directory listing HTML generation.
12-
* Removed dependencies on `handlebars` and `chrono`
14+
* Added security notes for tap_req middleware (#167)
15+
* Added safety notes for wrap middleware (#167)
16+
* Improved performance of the entire middleware pipeline, reducing heap allocations (#167)
17+
* Unused Next/NextFn are now zero-alloc (#167)
18+
* Refactored directory listing HTML generation. (#165)
19+
* Removed dependencies on `handlebars` and `chrono` (#165)
1320

1421
## 0.8.5
1522

volga/src/app/pipeline.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,7 @@ impl Pipeline {
130130

131131
#[cfg(feature = "middleware")]
132132
pub(crate) async fn execute(&self, ctx: HttpContext) -> HttpResult {
133-
let next = &self.start;
134-
if let Some(next) = next {
135-
let next: NextFn = next.clone();
133+
if let Some(next) = &self.start {
136134
next(ctx).await
137135
} else {
138136
ctx.execute().await

volga/src/middleware.rs

Lines changed: 80 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,9 @@ impl Middlewares {
100100
let last = iter.next()?;
101101
let mut next: NextFn = {
102102
let handler = last.clone();
103-
// Call the last middleware, ignoring its `next` argument with an empty placeholder
104-
Arc::new(move |ctx|
105-
handler(ctx, Arc::new(|_| Box::pin(async { not_found!() }))))
103+
// Allocate the placeholder once at compose time, not per-request
104+
let dummy: NextFn = Arc::new(|_| Box::pin(async { not_found!() }));
105+
Arc::new(move |ctx| handler(ctx, dummy.clone()))
106106
};
107107

108108
for mw in iter {
@@ -133,6 +133,31 @@ impl App {
133133
///# app.run().await
134134
///# }
135135
/// ```
136+
///
137+
/// # Timeouts
138+
///
139+
/// The pipeline does not enforce per-request timeouts. If your middleware
140+
/// performs a long-running or potentially unbounded operation, check the
141+
/// [`CancellationToken`](crate::CancellationToken) injected into each
142+
/// request's extensions to avoid holding connections open indefinitely:
143+
///
144+
/// ```no_run
145+
/// use volga::{App, CancellationToken, error::Error};
146+
///
147+
///# #[tokio::main]
148+
///# async fn main() -> std::io::Result<()> {
149+
/// let mut app = App::new();
150+
///
151+
/// app.wrap(|ctx, next| async move {
152+
/// let token = ctx.extract::<CancellationToken>()?;
153+
/// tokio::select! {
154+
/// res = next(ctx) => res,
155+
/// _ = token.cancelled() => Err(Error::server_error("request cancelled")),
156+
/// }
157+
/// });
158+
///# app.run().await
159+
///# }
160+
/// ```
136161
pub fn wrap<F, Fut>(&mut self, middleware: F) -> &mut Self
137162
where
138163
F: Fn(HttpContext, NextFn) -> Fut + Send + Sync + 'static,
@@ -213,6 +238,14 @@ impl App {
213238
///# app.run().await
214239
///# }
215240
/// ```
241+
///
242+
/// # Security
243+
///
244+
/// `tap_req` grants full mutable ownership of the incoming request, including
245+
/// all headers. Security-critical values such as `Authorization` can be
246+
/// stripped or overwritten before downstream middleware and handlers observe
247+
/// them. Only register trusted closures and be mindful that registration order
248+
/// determines which code sees the original request.
216249
#[cfg(feature = "di")]
217250
pub fn tap_req<F, Args, R>(&mut self, map: F) -> &mut Self
218251
where
@@ -259,6 +292,14 @@ impl App {
259292
///# app.run().await
260293
///# }
261294
/// ```
295+
///
296+
/// # Security
297+
///
298+
/// `tap_req` grants full mutable ownership of the incoming request, including
299+
/// all headers. Security-critical values such as `Authorization` can be
300+
/// stripped or overwritten before downstream middleware and handlers observe
301+
/// them. Only register trusted closures and be mindful that registration order
302+
/// determines which code sees the original request.
262303
#[cfg(not(feature = "di"))]
263304
pub fn tap_req<F, R>(&mut self, map: F) -> &mut Self
264305
where
@@ -469,6 +510,14 @@ impl<'a> Route<'a> {
469510
///# app.run().await
470511
///# }
471512
/// ```
513+
///
514+
/// # Security
515+
///
516+
/// `tap_req` grants full mutable ownership of the incoming request, including
517+
/// all headers. Security-critical values such as `Authorization` can be
518+
/// stripped or overwritten before downstream middleware and handlers observe
519+
/// them. Only register trusted closures and be mindful that registration order
520+
/// determines which code sees the original request.
472521
#[cfg(feature = "di")]
473522
pub fn tap_req<F, Args, R>(self, map: F) -> Self
474523
where
@@ -513,6 +562,14 @@ impl<'a> Route<'a> {
513562
///# app.run().await
514563
///# }
515564
/// ```
565+
///
566+
/// # Security
567+
///
568+
/// `tap_req` grants full mutable ownership of the incoming request, including
569+
/// all headers. Security-critical values such as `Authorization` can be
570+
/// stripped or overwritten before downstream middleware and handlers observe
571+
/// them. Only register trusted closures and be mindful that registration order
572+
/// determines which code sees the original request.
516573
#[cfg(not(feature = "di"))]
517574
pub fn tap_req<F, R>(self, map: F) -> Self
518575
where
@@ -729,15 +786,23 @@ impl<'a> RouteGroup<'a> {
729786
///# app.run().await
730787
///# }
731788
/// ```
789+
///
790+
/// # Security
791+
///
792+
/// `tap_req` grants full mutable ownership of the incoming request, including
793+
/// all headers. Security-critical values such as `Authorization` can be
794+
/// stripped or overwritten before downstream middleware and handlers observe
795+
/// them. Only register trusted closures and be mindful that registration order
796+
/// determines which code sees the original request.
732797
#[cfg(feature = "di")]
733798
pub fn tap_req<F, Args, R>(&mut self, map: F) -> &mut Self
734799
where
735800
F: TapReqHandler<Args, Output = R>,
736801
R: IntoTapResult,
737802
Args: FromContainer + Send + 'static,
738803
{
739-
let map_err_fn = make_tap_req_fn(map);
740-
self.middleware.push(map_err_fn);
804+
let tap_req_fn = make_tap_req_fn(map);
805+
self.middleware.push(tap_req_fn);
741806
self
742807
}
743808

@@ -776,14 +841,22 @@ impl<'a> RouteGroup<'a> {
776841
///# app.run().await
777842
///# }
778843
/// ```
844+
///
845+
/// # Security
846+
///
847+
/// `tap_req` grants full mutable ownership of the incoming request, including
848+
/// all headers. Security-critical values such as `Authorization` can be
849+
/// stripped or overwritten before downstream middleware and handlers observe
850+
/// them. Only register trusted closures and be mindful that registration order
851+
/// determines which code sees the original request.
779852
#[cfg(not(feature = "di"))]
780853
pub fn tap_req<F, R>(&mut self, map: F) -> &mut Self
781854
where
782855
F: TapReqHandler<Output = R>,
783856
R: IntoTapResult,
784857
{
785-
let map_err_fn = make_tap_req_fn(map);
786-
self.middleware.push(map_err_fn);
858+
let tap_req_fn = make_tap_req_fn(map);
859+
self.middleware.push(tap_req_fn);
787860
self
788861
}
789862

volga/src/middleware/handler.rs

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! Extractors for middleware functions
22
3+
use futures_util::future::BoxFuture;
34
use std::future::Future;
45
use std::pin::Pin;
56
use std::task::{Context, Poll};
@@ -8,9 +9,23 @@ use crate::error::Error;
89
use crate::{HttpRequestMut, HttpResponse, HttpResult};
910
use super::{HttpContext, NextFn};
1011

12+
/// Internal state machine for [`Next`]
13+
///
14+
/// `Pending` is intentionally large: `HttpContext` lives here until the first
15+
/// poll, avoiding the heap allocation that would be required to box it.
16+
/// Both variants reside inside the already heap-allocated [`Next`] future,
17+
/// so this does not create stack pressure.
18+
#[allow(clippy::large_enum_variant)]
19+
enum NextState {
20+
/// Not yet polled; the inner future is created on demand
21+
Pending(HttpContext, NextFn),
22+
/// Polled at least once; holds the running future
23+
Running(BoxFuture<'static, HttpResult>),
24+
}
25+
1126
/// Represents the [`Future`] that wraps the next middleware in the chain,
1227
/// that will be called by `await` with the current [`HttpContext`]
13-
///
28+
///
1429
/// # Example
1530
/// ```no_run
1631
/// # use volga::middleware::Next;
@@ -21,7 +36,7 @@ use super::{HttpContext, NextFn};
2136
/// });
2237
/// ```
2338
pub struct Next {
24-
inner: Option<Pin<Box<dyn Future<Output = HttpResult> + Send>>>
39+
state: Option<NextState>
2540
}
2641

2742
impl std::fmt::Debug for Next {
@@ -37,24 +52,35 @@ impl Future for Next {
3752
#[inline]
3853
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
3954
let this = self.get_mut();
40-
let fut = this
41-
.inner
42-
.as_mut()
43-
.ok_or_else(|| Error::server_error("Next polled after completion"))?;
44-
45-
let poll = fut.as_mut().poll(cx);
46-
if matches!(poll, Poll::Ready(_)) {
47-
this.inner = None;
55+
match this.state.take() {
56+
None => Poll::Ready(Err(Error::server_error("Next polled after completion"))),
57+
Some(NextState::Pending(ctx, next)) => {
58+
let mut fut = next(ctx);
59+
let poll = fut.as_mut().poll(cx);
60+
if poll.is_pending() {
61+
this.state = Some(NextState::Running(fut));
62+
}
63+
poll
64+
}
65+
Some(NextState::Running(mut fut)) => {
66+
let poll = fut.as_mut().poll(cx);
67+
if poll.is_pending() {
68+
this.state = Some(NextState::Running(fut));
69+
}
70+
poll
71+
}
4872
}
49-
poll
5073
}
5174
}
5275

5376
impl Next {
54-
/// Creates a new [`Next`]
77+
/// Creates a new [`Next`].
78+
///
79+
/// The inner future is created lazily: `next(ctx)` is not called until
80+
/// this future is first polled. This avoids a heap allocation when the
81+
/// middleware exits early without awaiting `next`.
5582
pub fn new(ctx: HttpContext, next: NextFn) -> Self {
56-
Self { inner: Some(Box::pin(next(ctx))) }
57-
//Self { ctx: Some(ctx), next }
83+
Self { state: Some(NextState::Pending(ctx, next)) }
5884
}
5985
}
6086

@@ -166,12 +192,12 @@ mod tests {
166192
use futures_util::task::noop_waker_ref;
167193
use crate::{HttpBody, HttpResponse, status};
168194
use crate::error::Error;
169-
use super::{MapOkHandler, MiddlewareHandler, Next};
195+
use super::{MapOkHandler, MiddlewareHandler, Next, NextState};
170196

171197
#[test]
172198
fn next_returns_error_when_polled_after_completion() {
173199
let mut next = Next {
174-
inner: Some(Box::pin(async { status!(204) })),
200+
state: Some(NextState::Running(Box::pin(async { status!(204) }))),
175201
};
176202

177203
let waker = noop_waker_ref();
@@ -194,7 +220,7 @@ mod tests {
194220
#[tokio::test]
195221
async fn middleware_handler_invokes_function_with_next() {
196222
let next = Next {
197-
inner: Some(Box::pin(async { status!(204) })),
223+
state: Some(NextState::Running(Box::pin(async { status!(204) }))),
198224
};
199225

200226
let handler = |value: u8, next: Next| async move {

0 commit comments

Comments
 (0)