From aa2970fa508908534fc3264bd6e5f3c519040651 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Tue, 22 Oct 2024 14:03:57 +0800 Subject: [PATCH 01/22] feat(http): add multipart for server --- Cargo.lock | 173 ++++++++- Cargo.toml | 3 + volo-http/Cargo.toml | 9 +- volo-http/src/server/extract.rs | 1 + volo-http/src/server/layer.rs | 335 ----------------- volo-http/src/server/layer/body_limit.rs | 104 ++++++ volo-http/src/server/layer/filter.rs | 180 +++++++++ volo-http/src/server/layer/mod.rs | 11 + volo-http/src/server/layer/timeout.rs | 177 +++++++++ volo-http/src/server/utils/mod.rs | 2 + volo-http/src/server/utils/multipart.rs | 443 +++++++++++++++++++++++ volo-http/src/server/utils/ws.rs | 2 +- 12 files changed, 1097 insertions(+), 343 deletions(-) delete mode 100644 volo-http/src/server/layer.rs create mode 100644 volo-http/src/server/layer/body_limit.rs create mode 100644 volo-http/src/server/layer/filter.rs create mode 100644 volo-http/src/server/layer/mod.rs create mode 100644 volo-http/src/server/layer/timeout.rs create mode 100644 volo-http/src/server/utils/multipart.rs diff --git a/Cargo.lock b/Cargo.lock index d074e385..e8349c72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1236,6 +1236,23 @@ dependencies = [ "tokio-rustls 0.24.1", ] +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http 1.1.0", + "hyper 1.4.1", + "hyper-util", + "rustls 0.23.13", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.0", + "tower-service", +] + [[package]] name = "hyper-timeout" version = "0.5.1" @@ -1249,6 +1266,22 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.4.1", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.8" @@ -1673,6 +1706,23 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http 1.1.0", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + [[package]] name = "mur3" version = "0.1.0" @@ -2420,7 +2470,7 @@ dependencies = [ "http 0.2.12", "http-body 0.4.6", "hyper 0.14.30", - "hyper-rustls", + "hyper-rustls 0.24.2", "ipnet", "js-sys", "log", @@ -2434,7 +2484,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper 0.1.2", - "system-configuration", + "system-configuration 0.5.1", "tokio", "tokio-rustls 0.24.1", "tower-service", @@ -2446,6 +2496,50 @@ dependencies = [ "winreg", ] +[[package]] +name = "reqwest" +version = "0.12.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.4.6", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.4.1", + "hyper-rustls 0.27.3", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile 2.1.3", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "system-configuration 0.6.1", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry", +] + [[package]] name = "resolv-conf" version = "0.7.0" @@ -2928,6 +3022,9 @@ name = "sync_wrapper" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] [[package]] name = "sysinfo" @@ -2951,7 +3048,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation", - "system-configuration-sys", + "system-configuration-sys 0.5.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "system-configuration-sys 0.6.0", ] [[package]] @@ -2964,6 +3072,16 @@ dependencies = [ "libc", ] +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.12.0" @@ -3138,6 +3256,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls 0.23.13", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.16" @@ -3486,7 +3615,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f8811797a24ff123db3c6e1087aa42551d03d772b3724be421ad063da1f5f3f" dependencies = [ "directories", - "reqwest", + "reqwest 0.11.27", "semver", "serde", "serde_json", @@ -3721,9 +3850,12 @@ dependencies = [ "mime", "mime_guess", "motore", + "multer", "parking_lot 0.12.3", "paste", "pin-project", + "rand", + "reqwest 0.12.8", "scopeguard", "serde", "serde_urlencoded", @@ -3738,6 +3870,7 @@ dependencies = [ "tokio-util", "tracing", "tungstenite", + "url", "volo", ] @@ -3966,7 +4099,7 @@ checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" dependencies = [ "windows-implement", "windows-interface", - "windows-result", + "windows-result 0.1.2", "windows-targets 0.52.6", ] @@ -3992,6 +4125,17 @@ dependencies = [ "syn 2.0.77", ] +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result 0.2.0", + "windows-strings", + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.1.2" @@ -4001,6 +4145,25 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 6886bb77..6bef9af8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -123,6 +123,9 @@ tracing-subscriber = "0.3" update-informer = "1" url_path = "0.1" walkdir = "2" +multer = "3.1.0" +reqwest = "0.12.8" +url="2.5.2" # Optional dependencies rustls = "0.22" diff --git a/volo-http/Cargo.toml b/volo-http/Cargo.toml index 825262ab..24599f7a 100644 --- a/volo-http/Cargo.toml +++ b/volo-http/Cargo.toml @@ -58,6 +58,7 @@ tokio = { workspace = true, features = [ ] } tokio-util = { workspace = true, features = ["io"] } tracing.workspace = true +multer.workspace = true # =====optional===== @@ -85,14 +86,17 @@ async-stream.workspace = true libc.workspace = true serde = { workspace = true, features = ["derive"] } tokio-test.workspace = true +reqwest = { workspace = true, features = ["multipart"] } +url.workspace = true +rand.workspace = true [features] default = [] default_client = ["client", "json"] -default_server = ["server", "query", "form", "json"] +default_server = ["server", "query", "form", "json", "multipart"] -full = ["client", "server", "rustls", "cookie", "query", "form", "json", "tls", "ws"] +full = ["client", "server", "rustls", "cookie", "query", "form", "json", "multipart", "tls", "ws"] client = ["hyper/client", "hyper/http1"] # client core server = ["hyper/server", "hyper/http1", "dep:matchit"] # server core @@ -111,6 +115,7 @@ __serde = ["dep:serde"] # a private feature for enabling `serde` by `serde_xxx` query = ["__serde", "dep:serde_urlencoded"] form = ["__serde", "dep:serde_urlencoded"] json = ["__serde", "dep:sonic-rs"] +multipart = [] [package.metadata.docs.rs] all-features = true diff --git a/volo-http/src/server/extract.rs b/volo-http/src/server/extract.rs index e054939a..5dc62a7e 100644 --- a/volo-http/src/server/extract.rs +++ b/volo-http/src/server/extract.rs @@ -408,6 +408,7 @@ where parts: Parts, body: B, ) -> Result { + // TODO: add limited body let bytes = body .collect() .await diff --git a/volo-http/src/server/layer.rs b/volo-http/src/server/layer.rs deleted file mode 100644 index 8341995a..00000000 --- a/volo-http/src/server/layer.rs +++ /dev/null @@ -1,335 +0,0 @@ -//! Collections of some useful [`Layer`]s. -//! -//! See [`FilterLayer`] and [`TimeoutLayer`] for more details. - -use std::{marker::PhantomData, time::Duration}; - -use motore::{layer::Layer, service::Service}; - -use super::{handler::HandlerWithoutRequest, IntoResponse}; -use crate::{context::ServerContext, request::ServerRequest, response::ServerResponse}; - -/// [`Layer`] for filtering requests -/// -/// See [`FilterLayer::new`] for more details. -#[derive(Clone)] -pub struct FilterLayer { - handler: H, - _marker: PhantomData<(R, T)>, -} - -impl FilterLayer { - /// Create a new [`FilterLayer`] - /// - /// The `handler` is an async function with some params that implement - /// [`FromContext`](crate::server::extract::FromContext), and returns - /// `Result<(), impl IntoResponse>`. - /// - /// If the handler returns `Ok(())`, the request will proceed. However, if the handler returns - /// `Err` with an object that implements [`IntoResponse`], the request will be rejected with - /// the returned object as the response. - /// - /// # Examples - /// - /// ``` - /// use http::{method::Method, status::StatusCode}; - /// use volo_http::server::{ - /// layer::FilterLayer, - /// route::{get, Router}, - /// }; - /// - /// async fn reject_post(method: Method) -> Result<(), StatusCode> { - /// if method == Method::POST { - /// Err(StatusCode::METHOD_NOT_ALLOWED) - /// } else { - /// Ok(()) - /// } - /// } - /// - /// async fn handler() -> &'static str { - /// "Hello, World" - /// } - /// - /// let router: Router = Router::new() - /// .route("/", get(handler)) - /// .layer(FilterLayer::new(reject_post)); - /// ``` - pub fn new(h: H) -> Self { - Self { - handler: h, - _marker: PhantomData, - } - } -} - -impl Layer for FilterLayer -where - S: Send + Sync + 'static, - H: Clone + Send + Sync + 'static, - T: Sync, -{ - type Service = Filter; - - fn layer(self, inner: S) -> Self::Service { - Filter { - service: inner, - handler: self.handler, - _marker: PhantomData, - } - } -} - -/// [`FilterLayer`] generated [`Service`] -/// -/// See [`FilterLayer`] for more details. -#[derive(Clone)] -pub struct Filter { - service: S, - handler: H, - _marker: PhantomData<(R, T)>, -} - -impl Service> for Filter -where - S: Service> + Send + Sync + 'static, - S::Response: IntoResponse, - S::Error: IntoResponse, - B: Send, - H: HandlerWithoutRequest> + Clone + Send + Sync + 'static, - R: IntoResponse + Send + Sync, - T: Sync, -{ - type Response = ServerResponse; - type Error = S::Error; - - async fn call( - &self, - cx: &mut ServerContext, - req: ServerRequest, - ) -> Result { - let (mut parts, body) = req.into_parts(); - let res = self.handler.clone().handle(cx, &mut parts).await; - let req = ServerRequest::from_parts(parts, body); - match res { - // do not filter it, call the service - Ok(Ok(())) => self - .service - .call(cx, req) - .await - .map(IntoResponse::into_response), - // filter it and return the specified response - Ok(Err(res)) => Ok(res.into_response()), - // something wrong while extracting - Err(rej) => { - tracing::warn!("[Volo-HTTP] FilterLayer: something wrong while extracting"); - Ok(rej.into_response()) - } - } - } -} - -/// [`Layer`] for setting timeout to the request -/// -/// See [`TimeoutLayer::new`] for more details. -#[derive(Clone)] -pub struct TimeoutLayer { - duration: Duration, - handler: H, -} - -impl TimeoutLayer { - /// Create a new [`TimeoutLayer`] with given [`Duration`] and handler. - /// - /// The handler should be a sync function with [`&ServerContext`](ServerContext) as parameter, - /// and return anything that implement [`IntoResponse`]. - /// - /// # Examples - /// - /// ``` - /// use std::time::Duration; - /// - /// use http::status::StatusCode; - /// use volo_http::{ - /// context::ServerContext, - /// server::{ - /// layer::TimeoutLayer, - /// route::{get, Router}, - /// }, - /// }; - /// - /// async fn index() -> &'static str { - /// "Hello, World" - /// } - /// - /// fn timeout_handler(_: &ServerContext) -> StatusCode { - /// StatusCode::REQUEST_TIMEOUT - /// } - /// - /// let router: Router = Router::new() - /// .route("/", get(index)) - /// .layer(TimeoutLayer::new(Duration::from_secs(1), timeout_handler)); - /// ``` - pub fn new(duration: Duration, handler: H) -> Self { - Self { duration, handler } - } -} - -impl Layer for TimeoutLayer -where - S: Send + Sync + 'static, -{ - type Service = Timeout; - - fn layer(self, inner: S) -> Self::Service { - Timeout { - service: inner, - duration: self.duration, - handler: self.handler, - } - } -} - -trait TimeoutHandler<'r> { - fn call(self, cx: &'r ServerContext) -> ServerResponse; -} - -impl<'r, F, R> TimeoutHandler<'r> for F -where - F: FnOnce(&'r ServerContext) -> R + 'r, - R: IntoResponse + 'r, -{ - fn call(self, cx: &'r ServerContext) -> ServerResponse { - self(cx).into_response() - } -} - -/// [`TimeoutLayer`] generated [`Service`] -/// -/// See [`TimeoutLayer`] for more details. -#[derive(Clone)] -pub struct Timeout { - service: S, - duration: Duration, - handler: H, -} - -impl Service> for Timeout -where - S: Service> + Send + Sync + 'static, - S::Response: IntoResponse, - S::Error: IntoResponse, - B: Send, - H: for<'r> TimeoutHandler<'r> + Clone + Sync, -{ - type Response = ServerResponse; - type Error = S::Error; - - async fn call( - &self, - cx: &mut ServerContext, - req: ServerRequest, - ) -> Result { - let fut_service = self.service.call(cx, req); - let fut_timeout = tokio::time::sleep(self.duration); - - tokio::select! { - resp = fut_service => resp.map(IntoResponse::into_response), - _ = fut_timeout => { - Ok((self.handler.clone()).call(cx)) - }, - } - } -} - -#[cfg(test)] -mod layer_tests { - use http::{method::Method, status::StatusCode}; - use motore::{layer::Layer, service::Service}; - - use crate::{ - body::BodyConversion, - context::ServerContext, - server::{ - route::{any, get, Route}, - test_helpers::empty_cx, - }, - utils::test_helpers::simple_req, - }; - - #[tokio::test] - async fn test_filter_layer() { - use crate::server::layer::FilterLayer; - - async fn reject_post(method: Method) -> Result<(), StatusCode> { - if method == Method::POST { - Err(StatusCode::METHOD_NOT_ALLOWED) - } else { - Ok(()) - } - } - - async fn handler() -> &'static str { - "Hello, World" - } - - let filter_layer = FilterLayer::new(reject_post); - let route: Route<&str> = Route::new(any(handler)); - let service = filter_layer.layer(route); - - let mut cx = empty_cx(); - - // Test case 1: not filter - let req = simple_req(Method::GET, "/", ""); - let resp = service.call(&mut cx, req).await.unwrap(); - assert_eq!( - resp.into_body().into_string().await.unwrap(), - "Hello, World" - ); - - // Test case 2: filter - let req = simple_req(Method::POST, "/", ""); - let resp = service.call(&mut cx, req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); - } - - #[tokio::test] - async fn test_timeout_layer() { - use std::time::Duration; - - use crate::server::layer::TimeoutLayer; - - async fn index_handler() -> &'static str { - "Hello, World" - } - - async fn index_timeout_handler() -> &'static str { - tokio::time::sleep(Duration::from_secs_f64(1.5)).await; - "Hello, World" - } - - fn timeout_handler(_: &ServerContext) -> StatusCode { - StatusCode::REQUEST_TIMEOUT - } - - let timeout_layer = TimeoutLayer::new(Duration::from_secs(1), timeout_handler); - - let mut cx = empty_cx(); - - // Test case 1: timeout - let route: Route<&str> = Route::new(get(index_timeout_handler)); - let service = timeout_layer.clone().layer(route); - let req = simple_req(Method::GET, "/", ""); - let resp = service.call(&mut cx, req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::REQUEST_TIMEOUT); - - // Test case 2: not timeout - let route: Route<&str> = Route::new(get(index_handler)); - let service = timeout_layer.clone().layer(route); - let req = simple_req(Method::GET, "/", ""); - let resp = service.call(&mut cx, req).await.unwrap(); - assert_eq!( - resp.into_body().into_string().await.unwrap(), - "Hello, World" - ); - } -} diff --git a/volo-http/src/server/layer/body_limit.rs b/volo-http/src/server/layer/body_limit.rs new file mode 100644 index 00000000..5751bde8 --- /dev/null +++ b/volo-http/src/server/layer/body_limit.rs @@ -0,0 +1,104 @@ +use motore::{layer::Layer, Service}; + +use crate::{ + context::ServerContext, request::ServerRequest, response::ServerResponse, server::IntoResponse, +}; + +#[derive(Debug, Clone, Copy)] +pub(crate) enum BodyLimitKind { + #[allow(dead_code)] + Disable, + Limit(usize), +} + +/// [`Layer`] for limiting body size +/// +/// Currently only supports [`Multipart`](crate::server::utils::multipart::Multipart) extractor. +/// +/// See [`BodyLimitLayer::max`] for more details. +#[derive(Clone)] +pub struct BodyLimitLayer { + kind: BodyLimitKind, +} + +impl BodyLimitLayer { + /// Create a new [`BodyLimitLayer`] with given [`body_limit`]. + /// + /// If the Body is larger than the `body_limit`, the request will be rejected. + /// + /// # Examples + /// + /// ``` + /// use http::StatusCode; + /// use volo_http::server::{ + /// layer::BodyLimitLayer, + /// route::{post, Router}, + /// }; + /// + /// async fn handler() -> &'static str { + /// "Hello, World" + /// } + /// + /// let router: Router = Router::new() + /// .route("/", post(handler)) + /// .layer(BodyLimitLayer::max(1024)); // limit body size to 1KB + /// ``` + pub fn max(body_limit: usize) -> Self { + Self { + kind: BodyLimitKind::Limit(body_limit), + } + } + + /// Create a new [`BodyLimitLayer`] with `body_limit` disabled. + /// + /// It's unnecessary to use this method, because the `body_limit` is disabled by default. + #[allow(dead_code)] + fn disable() -> Self { + Self { + kind: BodyLimitKind::Disable, + } + } +} + +impl Layer for BodyLimitLayer +where + S: Send + Sync + 'static, +{ + type Service = BodyLimitService; + + fn layer(self, inner: S) -> Self::Service { + BodyLimitService { + service: inner, + kind: self.kind, + } + } +} + +/// [`BodyLimitLayer`] generated [`Service`] +/// +/// See [`BodyLimitLayer`] for more details. +#[derive(Clone)] +pub struct BodyLimitService { + service: S, + kind: BodyLimitKind, +} + +impl Service> for BodyLimitService +where + S: Service> + Send + Sync + 'static, + S::Response: IntoResponse, + S::Error: IntoResponse, + B: Send, +{ + type Response = ServerResponse; + type Error = S::Error; + + async fn call( + &self, + cx: &mut ServerContext, + mut req: ServerRequest, + ) -> Result { + req.extensions_mut().insert(self.kind); + Ok(self.service.call(cx, req).await?.into_response()) + } +} diff --git a/volo-http/src/server/layer/filter.rs b/volo-http/src/server/layer/filter.rs new file mode 100644 index 00000000..cb56797e --- /dev/null +++ b/volo-http/src/server/layer/filter.rs @@ -0,0 +1,180 @@ +use std::marker::PhantomData; + +use motore::{layer::Layer, Service}; + +use crate::{ + context::ServerContext, + request::ServerRequest, + response::ServerResponse, + server::{handler::HandlerWithoutRequest, IntoResponse}, +}; + +/// [`Layer`] for filtering requests +/// +/// See [`FilterLayer::new`] for more details. +#[derive(Clone)] +pub struct FilterLayer { + handler: H, + _marker: PhantomData<(R, T)>, +} + +impl FilterLayer { + /// Create a new [`FilterLayer`] + /// + /// The `handler` is an async function with some params that implement + /// [`FromContext`](crate::server::extract::FromContext), and returns + /// `Result<(), impl IntoResponse>`. + /// + /// If the handler returns `Ok(())`, the request will proceed. However, if the handler returns + /// `Err` with an object that implements [`IntoResponse`], the request will be rejected with + /// the returned object as the response. + /// + /// # Examples + /// + /// ``` + /// use http::{method::Method, status::StatusCode}; + /// use volo_http::server::{ + /// layer::FilterLayer, + /// route::{get, Router}, + /// }; + /// + /// async fn reject_post(method: Method) -> Result<(), StatusCode> { + /// if method == Method::POST { + /// Err(StatusCode::METHOD_NOT_ALLOWED) + /// } else { + /// Ok(()) + /// } + /// } + /// + /// async fn handler() -> &'static str { + /// "Hello, World" + /// } + /// + /// let router: Router = Router::new() + /// .route("/", get(handler)) + /// .layer(FilterLayer::new(reject_post)); + /// ``` + pub fn new(h: H) -> Self { + Self { + handler: h, + _marker: PhantomData, + } + } +} + +impl Layer for FilterLayer +where + S: Send + Sync + 'static, + H: Clone + Send + Sync + 'static, + T: Sync, +{ + type Service = Filter; + + fn layer(self, inner: S) -> Self::Service { + Filter { + service: inner, + handler: self.handler, + _marker: PhantomData, + } + } +} + +/// [`FilterLayer`] generated [`Service`] +/// +/// See [`FilterLayer`] for more details. +#[derive(Clone)] +pub struct Filter { + service: S, + handler: H, + _marker: PhantomData<(R, T)>, +} + +impl Service> for Filter +where + S: Service> + Send + Sync + 'static, + S::Response: IntoResponse, + S::Error: IntoResponse, + B: Send, + H: HandlerWithoutRequest> + Clone + Send + Sync + 'static, + R: IntoResponse + Send + Sync, + T: Sync, +{ + type Response = ServerResponse; + type Error = S::Error; + + async fn call( + &self, + cx: &mut ServerContext, + req: ServerRequest, + ) -> Result { + let (mut parts, body) = req.into_parts(); + let res = self.handler.clone().handle(cx, &mut parts).await; + let req = ServerRequest::from_parts(parts, body); + match res { + // do not filter it, call the service + Ok(Ok(())) => self + .service + .call(cx, req) + .await + .map(IntoResponse::into_response), + // filter it and return the specified response + Ok(Err(res)) => Ok(res.into_response()), + // something wrong while extracting + Err(rej) => { + tracing::warn!("[Volo-HTTP] FilterLayer: something wrong while extracting"); + Ok(rej.into_response()) + } + } + } +} + +#[cfg(test)] +mod filter_tests { + use http::{Method, StatusCode}; + use motore::{layer::Layer, Service}; + + use crate::{ + body::BodyConversion, + server::{ + route::{any, Route}, + test_helpers::empty_cx, + }, + utils::test_helpers::simple_req, + }; + + #[tokio::test] + async fn test_filter_layer() { + use crate::server::layer::FilterLayer; + + async fn reject_post(method: Method) -> Result<(), StatusCode> { + if method == Method::POST { + Err(StatusCode::METHOD_NOT_ALLOWED) + } else { + Ok(()) + } + } + + async fn handler() -> &'static str { + "Hello, World" + } + + let filter_layer = FilterLayer::new(reject_post); + let route: Route<&str> = Route::new(any(handler)); + let service = filter_layer.layer(route); + + let mut cx = empty_cx(); + + // Test case 1: not filter + let req = simple_req(Method::GET, "/", ""); + let resp = service.call(&mut cx, req).await.unwrap(); + assert_eq!( + resp.into_body().into_string().await.unwrap(), + "Hello, World" + ); + + // Test case 2: filter + let req = simple_req(Method::POST, "/", ""); + let resp = service.call(&mut cx, req).await.unwrap(); + assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); + } +} diff --git a/volo-http/src/server/layer/mod.rs b/volo-http/src/server/layer/mod.rs new file mode 100644 index 00000000..1b9fbcf4 --- /dev/null +++ b/volo-http/src/server/layer/mod.rs @@ -0,0 +1,11 @@ +//! Collections of some useful [`Layer`]s. +//! +//! See [`FilterLayer`] and [`TimeoutLayer`] for more details. + +pub(crate) mod body_limit; +mod filter; +mod timeout; + +pub use body_limit::BodyLimitLayer; +pub use filter::FilterLayer; +pub use timeout::TimeoutLayer; diff --git a/volo-http/src/server/layer/timeout.rs b/volo-http/src/server/layer/timeout.rs new file mode 100644 index 00000000..936206dc --- /dev/null +++ b/volo-http/src/server/layer/timeout.rs @@ -0,0 +1,177 @@ +use std::time::Duration; + +use motore::{layer::Layer, Service}; + +use crate::{ + context::ServerContext, request::ServerRequest, response::ServerResponse, server::IntoResponse, +}; + +/// [`Layer`] for setting timeout to the request +/// +/// See [`TimeoutLayer::new`] for more details. +#[derive(Clone)] +pub struct TimeoutLayer { + duration: Duration, + handler: H, +} + +impl TimeoutLayer { + /// Create a new [`TimeoutLayer`] with given [`Duration`] and handler. + /// + /// The handler should be a sync function with [`&ServerContext`](ServerContext) as parameter, + /// and return anything that implement [`IntoResponse`]. + /// + /// # Examples + /// + /// ``` + /// use std::time::Duration; + /// + /// use http::status::StatusCode; + /// use volo_http::{ + /// context::ServerContext, + /// server::{ + /// layer::TimeoutLayer, + /// route::{get, Router}, + /// }, + /// }; + /// + /// async fn index() -> &'static str { + /// "Hello, World" + /// } + /// + /// fn timeout_handler(_: &ServerContext) -> StatusCode { + /// StatusCode::REQUEST_TIMEOUT + /// } + /// + /// let router: Router = Router::new() + /// .route("/", get(index)) + /// .layer(TimeoutLayer::new(Duration::from_secs(1), timeout_handler)); + /// ``` + pub fn new(duration: Duration, handler: H) -> Self { + Self { duration, handler } + } +} + +impl Layer for TimeoutLayer +where + S: Send + Sync + 'static, +{ + type Service = Timeout; + + fn layer(self, inner: S) -> Self::Service { + Timeout { + service: inner, + duration: self.duration, + handler: self.handler, + } + } +} + +trait TimeoutHandler<'r> { + fn call(self, cx: &'r ServerContext) -> ServerResponse; +} + +impl<'r, F, R> TimeoutHandler<'r> for F +where + F: FnOnce(&'r ServerContext) -> R + 'r, + R: IntoResponse + 'r, +{ + fn call(self, cx: &'r ServerContext) -> ServerResponse { + self(cx).into_response() + } +} + +/// [`TimeoutLayer`] generated [`Service`] +/// +/// See [`TimeoutLayer`] for more details. +#[derive(Clone)] +pub struct Timeout { + service: S, + duration: Duration, + handler: H, +} + +impl Service> for Timeout +where + S: Service> + Send + Sync + 'static, + S::Response: IntoResponse, + S::Error: IntoResponse, + B: Send, + H: for<'r> TimeoutHandler<'r> + Clone + Sync, +{ + type Response = ServerResponse; + type Error = S::Error; + + async fn call( + &self, + cx: &mut ServerContext, + req: ServerRequest, + ) -> Result { + let fut_service = self.service.call(cx, req); + let fut_timeout = tokio::time::sleep(self.duration); + + tokio::select! { + resp = fut_service => resp.map(IntoResponse::into_response), + _ = fut_timeout => { + Ok((self.handler.clone()).call(cx)) + }, + } + } +} + +#[cfg(test)] +mod timeout_tests { + use http::{Method, StatusCode}; + use motore::{layer::Layer, Service}; + + use crate::{ + body::BodyConversion, + context::ServerContext, + server::{ + route::{get, Route}, + test_helpers::empty_cx, + }, + utils::test_helpers::simple_req, + }; + + #[tokio::test] + async fn test_timeout_layer() { + use std::time::Duration; + + use crate::server::layer::TimeoutLayer; + + async fn index_handler() -> &'static str { + "Hello, World" + } + + async fn index_timeout_handler() -> &'static str { + tokio::time::sleep(Duration::from_secs_f64(1.5)).await; + "Hello, World" + } + + fn timeout_handler(_: &ServerContext) -> StatusCode { + StatusCode::REQUEST_TIMEOUT + } + + let timeout_layer = TimeoutLayer::new(Duration::from_secs(1), timeout_handler); + + let mut cx = empty_cx(); + + // Test case 1: timeout + let route: Route<&str> = Route::new(get(index_timeout_handler)); + let service = timeout_layer.clone().layer(route); + let req = simple_req(Method::GET, "/", ""); + let resp = service.call(&mut cx, req).await.unwrap(); + assert_eq!(resp.status(), StatusCode::REQUEST_TIMEOUT); + + // Test case 2: not timeout + let route: Route<&str> = Route::new(get(index_handler)); + let service = timeout_layer.clone().layer(route); + let req = simple_req(Method::GET, "/", ""); + let resp = service.call(&mut cx, req).await.unwrap(); + assert_eq!( + resp.into_body().into_string().await.unwrap(), + "Hello, World" + ); + } +} diff --git a/volo-http/src/server/utils/mod.rs b/volo-http/src/server/utils/mod.rs index a1a456d1..38f75ff8 100644 --- a/volo-http/src/server/utils/mod.rs +++ b/volo-http/src/server/utils/mod.rs @@ -5,5 +5,7 @@ mod serve_dir; pub use file_response::FileResponse; pub use serve_dir::ServeDir; +#[cfg(feature = "multipart")] +pub mod multipart; #[cfg(feature = "ws")] pub mod ws; diff --git a/volo-http/src/server/utils/multipart.rs b/volo-http/src/server/utils/multipart.rs new file mode 100644 index 00000000..30c73134 --- /dev/null +++ b/volo-http/src/server/utils/multipart.rs @@ -0,0 +1,443 @@ +//! Multipart implementation for server. +//! +//! This module provides utilities for extracting `multipart/form-data` formatted data from HTTP +//! requests. +//! +//! # Example +//! +//! ```rust,no_run +//! use http::StatusCode; +//! use volo_http::{ +//! response::ServerResponse, +//! server::{ +//! route::post, +//! utils::multipart::{Multipart, MultipartRejectionError}, +//! }, +//! Router, +//! }; +//! +//! async fn upload( +//! mut multipart: Multipart<'static>, +//! ) -> Result { +//! while let Some(field) = multipart.next_field().await? { +//! let name = field.name().unwrap(); +//! let value = field.bytes().await?; +//! +//! println!("The field {} has {} bytes", name, value.len()); +//! } +//! +//! Ok(StatusCode::OK) +//! } +//! +//! let app: Router = Router::new().route("/upload", post(upload)); +//! ``` +//! +//! See [`Multipart`] for more details. + +use std::{error::Error, fmt, fmt::Debug}; + +use http::{request::Parts, StatusCode}; +use http_body_util::BodyExt; +use multer::Field; + +use crate::{ + context::ServerContext, + server::{extract::FromRequest, layer::body_limit::BodyLimitKind, IntoResponse}, +}; + +/// Extract a type from `multipart/form-data` HTTP requests. +/// +/// [`Multipart`] can be passed as an argument to a handler, which can be used to extract each +/// `multipart/form-data` field by calling [`Multipart::next_field`]. +/// +/// **Notice** +/// +/// Extracting `multipart/form-data` data will consume the body, hence [`Multipart`] must be the +/// last argument from the handler. +/// +/// # Example +/// +/// ```rust,no_run +/// use http::StatusCode; +/// use volo_http::{ +/// response::ServerResponse, +/// server::utils::multipart::{Multipart, MultipartRejectionError}, +/// }; +/// +/// async fn upload( +/// mut multipart: Multipart<'static>, +/// ) -> Result { +/// while let Some(field) = multipart.next_field().await? { +/// todo!() +/// } +/// +/// Ok(StatusCode::OK) +/// } +/// ``` +/// +/// # Body Limitation +/// +/// Since the body is unlimited, so it is recommended to use [`BodyLimitLayer`] to limit the size of +/// the body. +/// +/// ```rust,no_run +/// use http::StatusCode; +/// use volo_http::{ +/// Router, +/// server::{ +/// layer::BodyLimitLayer, +/// route::post, +/// utils::multipart::{Multipart, MultipartRejectionError}, +/// } +/// }; +/// +/// # async fn upload_handler(mut multipart: Multipart<'static>) -> Result { +/// # Ok(StatusCode::OK) +/// # } +/// +/// let app: Router<_>= Router::new() +/// .route("/",post(upload_handler)) +/// .layer( BodyLimitLayer::max(1024)); +/// ``` +#[must_use] +pub struct Multipart<'r> { + inner: multer::Multipart<'r>, +} + +impl<'r> Multipart<'r> { + /// Iterate over all [`Field`] in [`Multipart`] + /// + /// # Example + /// + /// ```rust,no_run + /// # use volo_http::server::utils::multipart::Multipart; + /// # let mut multipart: Multipart; + /// // Extract each field from multipart by using while loop + /// # async { + /// while let Some(field) = multipart.next_field().await? { + /// let name = field.name().unwrap().to_string(); // Get field name + /// let data = field.bytes().await?; // Get field data + /// # } + /// ``` + pub async fn next_field(&mut self) -> Result>, MultipartRejectionError> { + let field = self.inner.next_field().await?; + + if let Some(field) = field { + Ok(Some(field)) + } else { + Ok(None) + } + } +} + +impl<'r> FromRequest for Multipart<'r> { + type Rejection = MultipartRejectionError; + async fn from_request( + _: &mut ServerContext, + parts: Parts, + body: crate::body::Body, + ) -> Result { + let body = match parts.extensions.get::().copied() { + Some(BodyLimitKind::Disable) => body, + Some(BodyLimitKind::Limit(limit)) => { + crate::body::Body::from_body(http_body_util::Limited::new(body, limit)) + } + None => body, + }; + + let boundary = multer::parse_boundary( + parts + .headers + .get(http::header::CONTENT_TYPE) + .map(|h| h.to_str().unwrap_or_default()) + .unwrap_or_default(), + )?; + + let multipart = multer::Multipart::new(body.into_data_stream(), boundary); + + Ok(Self { inner: multipart }) + } +} + +/// [`Error`]s while extracting [`Multipart`]. +/// +/// [`Error`]: Error +#[derive(Debug)] +pub struct MultipartRejectionError { + inner: multer::Error, +} + +impl From for MultipartRejectionError { + fn from(err: multer::Error) -> Self { + Self { inner: err } + } +} + +fn status_code_from_multer_error(err: &multer::Error) -> StatusCode { + match err { + multer::Error::UnknownField { .. } + | multer::Error::IncompleteFieldData { .. } + | multer::Error::IncompleteHeaders + | multer::Error::ReadHeaderFailed(..) + | multer::Error::DecodeHeaderName { .. } + | multer::Error::DecodeContentType(..) + | multer::Error::NoBoundary + | multer::Error::DecodeHeaderValue { .. } + | multer::Error::NoMultipart + | multer::Error::IncompleteStream => StatusCode::BAD_REQUEST, + multer::Error::FieldSizeExceeded { .. } | multer::Error::StreamSizeExceeded { .. } => { + StatusCode::PAYLOAD_TOO_LARGE + } + multer::Error::StreamReadFailed(err) => { + if let Some(err) = err.downcast_ref::() { + return status_code_from_multer_error(err); + } + + if err + .downcast_ref::() + .is_some() + { + return StatusCode::PAYLOAD_TOO_LARGE; + } + + StatusCode::INTERNAL_SERVER_ERROR + } + _ => StatusCode::INTERNAL_SERVER_ERROR, + } +} + +impl MultipartRejectionError { + /// Convert the [`MultipartRejectionError`] into a [`http::StatusCode`]. + pub fn to_status_code(&self) -> http::StatusCode { + status_code_from_multer_error(&self.inner) + } +} + +impl Error for MultipartRejectionError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + Some(&self.inner) + } +} + +impl fmt::Display for MultipartRejectionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + std::fmt::Display::fmt(&self.inner, f) + } +} + +impl IntoResponse for MultipartRejectionError { + fn into_response(self) -> http::Response { + (self.to_status_code(), self.to_string()).into_response() + } +} + +#[cfg(test)] +mod multipart_tests { + use std::{ + convert::Infallible, + net::{IpAddr, Ipv4Addr, SocketAddr}, + }; + + use motore::Service; + use rand::Rng; + use reqwest::multipart::Form; + use volo::net::Address; + + use crate::{ + context::ServerContext, + request::ServerRequest, + response::ServerResponse, + server::{ + layer::BodyLimitLayer, + route::post, + test_helpers, + utils::multipart::{Multipart, MultipartRejectionError}, + IntoResponse, + }, + Router, Server, + }; + + fn _test_compile() { + async fn handler(_: Multipart<'_>) -> Result<(), Infallible> { + Ok(()) + } + let app = test_helpers::to_service(handler); + let addr = Address::Ip(SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + 8001, + )); + let _server = Server::new(app).run(addr); + } + + async fn run_handler(service: S, port: u16) + where + S: Service + + Send + + Sync + + 'static, + { + let addr = Address::Ip(SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + port, + )); + + tokio::spawn(Server::new(service).run(addr.clone())); + + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } + #[tokio::test] + async fn test_single_field_upload() { + const BYTES: &[u8] = "🦀".as_bytes(); + const FILE_NAME: &str = "index.html"; + const CONTENT_TYPE: &str = "text/html; charset=utf-8"; + + async fn handler(mut multipart: Multipart<'static>) -> impl IntoResponse { + let field = multipart.next_field().await.unwrap().unwrap(); + + assert_eq!(field.file_name().unwrap(), FILE_NAME); + assert_eq!(field.content_type().unwrap().as_ref(), CONTENT_TYPE); + assert_eq!(field.headers()["foo"], "bar"); + assert_eq!(field.bytes().await.unwrap(), BYTES); + + assert!(multipart.next_field().await.unwrap().is_none()); + } + + let form = Form::new().part( + "file", + reqwest::multipart::Part::bytes(BYTES) + .file_name(FILE_NAME) + .mime_str(CONTENT_TYPE) + .unwrap() + .headers(reqwest::header::HeaderMap::from_iter([( + reqwest::header::HeaderName::from_static("foo"), + reqwest::header::HeaderValue::from_static("bar"), + )])), + ); + + run_handler(test_helpers::to_service(handler), 8001).await; + + let url_str = format!("http://127.0.0.1:{}", 8001); + let url = url::Url::parse(url_str.as_str()).unwrap(); + + reqwest::Client::new() + .post(url.clone()) + .multipart(form) + .send() + .await + .unwrap(); + } + + #[tokio::test] + async fn test_multiple_field_upload() { + const BYTES: &[u8] = "🦀".as_bytes(); + const CONTENT_TYPE: &str = "text/html; charset=utf-8"; + + const FIELD_NAME1: &str = "file1"; + const FIELD_NAME2: &str = "file2"; + const FILE_NAME1: &str = "index1.html"; + const FILE_NAME2: &str = "index1.html"; + + async fn handler(mut multipart: Multipart<'static>) -> Result<(), MultipartRejectionError> { + while let Some(field) = multipart.next_field().await? { + match field.name() { + Some(FIELD_NAME1) => { + assert_eq!(field.file_name().unwrap(), FILE_NAME1); + assert_eq!(field.headers()["foo1"], "bar1"); + } + Some(FIELD_NAME2) => { + assert_eq!(field.file_name().unwrap(), FILE_NAME2); + assert_eq!(field.headers()["foo2"], "bar2"); + } + _ => unreachable!(), + } + assert_eq!(field.content_type().unwrap().as_ref(), CONTENT_TYPE); + assert_eq!(field.bytes().await?, BYTES); + } + + Ok(()) + } + + let form1 = Form::new().part( + FIELD_NAME1, + reqwest::multipart::Part::bytes(BYTES) + .file_name(FILE_NAME1) + .mime_str(CONTENT_TYPE) + .unwrap() + .headers(reqwest::header::HeaderMap::from_iter([( + reqwest::header::HeaderName::from_static("foo1"), + reqwest::header::HeaderValue::from_static("bar1"), + )])), + ); + let form2 = Form::new().part( + FIELD_NAME2, + reqwest::multipart::Part::bytes(BYTES) + .file_name(FILE_NAME2) + .mime_str(CONTENT_TYPE) + .unwrap() + .headers(reqwest::header::HeaderMap::from_iter([( + reqwest::header::HeaderName::from_static("foo2"), + reqwest::header::HeaderValue::from_static("bar2"), + )])), + ); + + run_handler(test_helpers::to_service(handler), 8002).await; + + let url_str = format!("http://127.0.0.1:{}", 8002); + let url = url::Url::parse(url_str.as_str()).unwrap(); + + for form in vec![form1, form2] { + reqwest::Client::new() + .post(url.clone()) + .multipart(form) + .send() + .await + .unwrap(); + } + } + + #[tokio::test] + async fn test_large_field_upload() { + async fn handler(mut multipart: Multipart<'static>) -> Result<(), MultipartRejectionError> { + while let Some(field) = multipart.next_field().await? { + field.bytes().await?; + } + + Ok(()) + } + + // generate random bytes + let mut rng = rand::thread_rng(); + let min_part_size = 4096; + let mut body = vec![0; min_part_size]; + rng.fill(&mut body[..]); + + let content_type = "text/html; charset=utf-8"; + let field_name = "file"; + let file_name = "index.html"; + + let form = Form::new().part( + field_name, + reqwest::multipart::Part::bytes(body) + .file_name(file_name) + .mime_str(content_type) + .unwrap(), + ); + + let app: Router<_> = Router::new() + .route("/", post(handler)) + .layer(BodyLimitLayer::max(1024)); + + run_handler(app, 8003).await; + + let url_str = format!("http://127.0.0.1:{}", 8003); + let url = url::Url::parse(url_str.as_str()).unwrap(); + + let resp = reqwest::Client::new() + .post(url.clone()) + .multipart(form) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), http::StatusCode::PAYLOAD_TOO_LARGE); + } +} diff --git a/volo-http/src/server/utils/ws.rs b/volo-http/src/server/utils/ws.rs index f4f584fa..a80fa7bf 100644 --- a/volo-http/src/server/utils/ws.rs +++ b/volo-http/src/server/utils/ws.rs @@ -71,7 +71,7 @@ use crate::{ const HEADERVALUE_UPGRADE: HeaderValue = HeaderValue::from_static("upgrade"); const HEADERVALUE_WEBSOCKET: HeaderValue = HeaderValue::from_static("websocket"); -/// Handler request for establishing WebSocket connection. +/// Handle request for establishing WebSocket connection. /// /// [`WebSocketUpgrade`] can be passed as an argument to a handler, which will be called if the /// http connection making the request can be upgraded to a websocket connection. From 92279c65518995dc11d9d69d44f6ad72f1544e30 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Tue, 22 Oct 2024 14:13:00 +0800 Subject: [PATCH 02/22] feat(http): add multipart for server --- volo-http/src/server/layer/body_limit.rs | 2 +- volo-http/src/server/layer/mod.rs | 2 +- volo-http/src/server/utils/multipart.rs | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/volo-http/src/server/layer/body_limit.rs b/volo-http/src/server/layer/body_limit.rs index 5751bde8..73ba6115 100644 --- a/volo-http/src/server/layer/body_limit.rs +++ b/volo-http/src/server/layer/body_limit.rs @@ -22,7 +22,7 @@ pub struct BodyLimitLayer { } impl BodyLimitLayer { - /// Create a new [`BodyLimitLayer`] with given [`body_limit`]. + /// Create a new [`BodyLimitLayer`] with given `body_limit`. /// /// If the Body is larger than the `body_limit`, the request will be rejected. /// diff --git a/volo-http/src/server/layer/mod.rs b/volo-http/src/server/layer/mod.rs index 1b9fbcf4..88c0cd8f 100644 --- a/volo-http/src/server/layer/mod.rs +++ b/volo-http/src/server/layer/mod.rs @@ -1,4 +1,4 @@ -//! Collections of some useful [`Layer`]s. +//! Collections of some useful `Layer`s. //! //! See [`FilterLayer`] and [`TimeoutLayer`] for more details. diff --git a/volo-http/src/server/utils/multipart.rs b/volo-http/src/server/utils/multipart.rs index 30c73134..44662e6b 100644 --- a/volo-http/src/server/utils/multipart.rs +++ b/volo-http/src/server/utils/multipart.rs @@ -77,7 +77,7 @@ use crate::{ /// /// # Body Limitation /// -/// Since the body is unlimited, so it is recommended to use [`BodyLimitLayer`] to limit the size of +/// Since the body is unlimited, so it is recommended to use [`BodyLimitLayer`](crate::server::layer::BodyLimitLayer) to limit the size of /// the body. /// /// ```rust,no_run @@ -117,6 +117,7 @@ impl<'r> Multipart<'r> { /// while let Some(field) = multipart.next_field().await? { /// let name = field.name().unwrap().to_string(); // Get field name /// let data = field.bytes().await?; // Get field data + /// } /// # } /// ``` pub async fn next_field(&mut self) -> Result>, MultipartRejectionError> { From 1e500044433d1d9d2f9b682663a091ac6b4b95cf Mon Sep 17 00:00:00 2001 From: StellarisW Date: Tue, 22 Oct 2024 14:14:42 +0800 Subject: [PATCH 03/22] feat(http): add multipart for server --- volo-http/src/server/utils/multipart.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volo-http/src/server/utils/multipart.rs b/volo-http/src/server/utils/multipart.rs index 44662e6b..9038cdf9 100644 --- a/volo-http/src/server/utils/multipart.rs +++ b/volo-http/src/server/utils/multipart.rs @@ -336,7 +336,7 @@ mod multipart_tests { const FIELD_NAME1: &str = "file1"; const FIELD_NAME2: &str = "file2"; const FILE_NAME1: &str = "index1.html"; - const FILE_NAME2: &str = "index1.html"; + const FILE_NAME2: &str = "index2.html"; async fn handler(mut multipart: Multipart<'static>) -> Result<(), MultipartRejectionError> { while let Some(field) = multipart.next_field().await? { From 7534dfb3417996cb2cfe5c7d349965cfb37d0805 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Tue, 22 Oct 2024 14:20:18 +0800 Subject: [PATCH 04/22] feat(http): add multipart for server --- volo-http/src/server/utils/multipart.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/volo-http/src/server/utils/multipart.rs b/volo-http/src/server/utils/multipart.rs index 9038cdf9..a0ffd131 100644 --- a/volo-http/src/server/utils/multipart.rs +++ b/volo-http/src/server/utils/multipart.rs @@ -77,8 +77,8 @@ use crate::{ /// /// # Body Limitation /// -/// Since the body is unlimited, so it is recommended to use [`BodyLimitLayer`](crate::server::layer::BodyLimitLayer) to limit the size of -/// the body. +/// Since the body is unlimited, so it is recommended to use +/// [`BodyLimitLayer`](crate::server::layer::BodyLimitLayer) to limit the size of the body. /// /// ```rust,no_run /// use http::StatusCode; @@ -114,10 +114,10 @@ impl<'r> Multipart<'r> { /// # let mut multipart: Multipart; /// // Extract each field from multipart by using while loop /// # async { - /// while let Some(field) = multipart.next_field().await? { + /// while let Some(field) = multipart.next_field().await? { /// let name = field.name().unwrap().to_string(); // Get field name /// let data = field.bytes().await?; // Get field data - /// } + /// } /// # } /// ``` pub async fn next_field(&mut self) -> Result>, MultipartRejectionError> { From eec1d9f4cabb549b140391a36c5b794975c4abb9 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Tue, 22 Oct 2024 20:53:44 +0800 Subject: [PATCH 05/22] feat(http): add multipart for server --- Cargo.toml | 6 ++-- volo-http/Cargo.toml | 7 +++-- volo-http/src/server/layer/body_limit.rs | 38 ++++++++++++++++-------- volo-http/src/server/utils/multipart.rs | 34 ++++++--------------- 4 files changed, 42 insertions(+), 43 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6bef9af8..36f3e518 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,6 +80,7 @@ mime = "0.3" mime_guess = { version = "2", default-features = false } mockall = "0.13" mockall_double = "0.3" +multer = "3.1.0" mur3 = "0.1" nix = "0.29" nom = "7" @@ -96,6 +97,7 @@ proc-macro2 = "1" quote = "1" rand = "0.8" regex = "1" +reqwest = "0.12.8" run_script = "0.10" rustc-hash = { version = "2", features = ["rand"] } same-file = "1" @@ -121,11 +123,9 @@ tower = "0.5" tracing = "0.1" tracing-subscriber = "0.3" update-informer = "1" +url="2.5.2" url_path = "0.1" walkdir = "2" -multer = "3.1.0" -reqwest = "0.12.8" -url="2.5.2" # Optional dependencies rustls = "0.22" diff --git a/volo-http/Cargo.toml b/volo-http/Cargo.toml index 24599f7a..0a1bae4d 100644 --- a/volo-http/Cargo.toml +++ b/volo-http/Cargo.toml @@ -58,10 +58,11 @@ tokio = { workspace = true, features = [ ] } tokio-util = { workspace = true, features = ["io"] } tracing.workspace = true -multer.workspace = true # =====optional===== +multer = { workspace = true, optional = true } + # server optional matchit = { workspace = true, optional = true } @@ -86,9 +87,9 @@ async-stream.workspace = true libc.workspace = true serde = { workspace = true, features = ["derive"] } tokio-test.workspace = true +rand.workspace = true reqwest = { workspace = true, features = ["multipart"] } url.workspace = true -rand.workspace = true [features] default = [] @@ -101,6 +102,7 @@ full = ["client", "server", "rustls", "cookie", "query", "form", "json", "multip client = ["hyper/client", "hyper/http1"] # client core server = ["hyper/server", "hyper/http1", "dep:matchit"] # server core +multipart = ["dep:multer"] ws = ["dep:tungstenite", "dep:tokio-tungstenite"] tls = ["rustls"] @@ -115,7 +117,6 @@ __serde = ["dep:serde"] # a private feature for enabling `serde` by `serde_xxx` query = ["__serde", "dep:serde_urlencoded"] form = ["__serde", "dep:serde_urlencoded"] json = ["__serde", "dep:sonic-rs"] -multipart = [] [package.metadata.docs.rs] all-features = true diff --git a/volo-http/src/server/layer/body_limit.rs b/volo-http/src/server/layer/body_limit.rs index 73ba6115..b1921a29 100644 --- a/volo-http/src/server/layer/body_limit.rs +++ b/volo-http/src/server/layer/body_limit.rs @@ -1,14 +1,17 @@ +use http::StatusCode; +use http_body::Body; use motore::{layer::Layer, Service}; -use crate::{ - context::ServerContext, request::ServerRequest, response::ServerResponse, server::IntoResponse, -}; +use crate::{context::ServerContext, request::ServerRequest}; +use crate::response::ServerResponse; +use crate::server::IntoResponse; #[derive(Debug, Clone, Copy)] pub(crate) enum BodyLimitKind { #[allow(dead_code)] Disable, - Limit(usize), + #[allow(dead_code)] + Block(usize), } /// [`Layer`] for limiting body size @@ -45,7 +48,7 @@ impl BodyLimitLayer { /// ``` pub fn max(body_limit: usize) -> Self { Self { - kind: BodyLimitKind::Limit(body_limit), + kind: BodyLimitKind::Block(body_limit), } } @@ -61,8 +64,6 @@ impl BodyLimitLayer { } impl Layer for BodyLimitLayer -where - S: Send + Sync + 'static, { type Service = BodyLimitService; @@ -77,18 +78,16 @@ where /// [`BodyLimitLayer`] generated [`Service`] /// /// See [`BodyLimitLayer`] for more details. -#[derive(Clone)] pub struct BodyLimitService { service: S, kind: BodyLimitKind, } -impl Service> for BodyLimitService +impl Service for BodyLimitService where - S: Service> + Send + Sync + 'static, + S: Service + Send + Sync + 'static, S::Response: IntoResponse, S::Error: IntoResponse, - B: Send, { type Response = ServerResponse; type Error = S::Error; @@ -96,8 +95,23 @@ where async fn call( &self, cx: &mut ServerContext, - mut req: ServerRequest, + req: ServerRequest, ) -> Result { + let (parts, body) = req.into_parts(); + if let BodyLimitKind::Block(limit) = self.kind { + // get body size from content length + if let Some(size) = parts.headers.get(http::header::CONTENT_LENGTH).and_then(|v| v.to_str().ok().and_then(|s| s.parse::().ok())) { + if size > limit { + return Ok(StatusCode::PAYLOAD_TOO_LARGE.into_response()); + } + } else { + // get body size from stream + if body.size_hint().lower() > limit as u64 { + return Ok(StatusCode::PAYLOAD_TOO_LARGE.into_response()); + } + } + } + let mut req = ServerRequest::from_parts(parts, body); req.extensions_mut().insert(self.kind); Ok(self.service.call(cx, req).await?.into_response()) } diff --git a/volo-http/src/server/utils/multipart.rs b/volo-http/src/server/utils/multipart.rs index a0ffd131..357021a6 100644 --- a/volo-http/src/server/utils/multipart.rs +++ b/volo-http/src/server/utils/multipart.rs @@ -34,7 +34,7 @@ //! //! See [`Multipart`] for more details. -use std::{error::Error, fmt, fmt::Debug}; +use std::{error::Error, fmt}; use http::{request::Parts, StatusCode}; use http_body_util::BodyExt; @@ -42,7 +42,7 @@ use multer::Field; use crate::{ context::ServerContext, - server::{extract::FromRequest, layer::body_limit::BodyLimitKind, IntoResponse}, + server::{extract::FromRequest, IntoResponse}, }; /// Extract a type from `multipart/form-data` HTTP requests. @@ -121,13 +121,7 @@ impl<'r> Multipart<'r> { /// # } /// ``` pub async fn next_field(&mut self) -> Result>, MultipartRejectionError> { - let field = self.inner.next_field().await?; - - if let Some(field) = field { - Ok(Some(field)) - } else { - Ok(None) - } + Ok(self.inner.next_field().await?) } } @@ -138,14 +132,6 @@ impl<'r> FromRequest for Multipart<'r> { parts: Parts, body: crate::body::Body, ) -> Result { - let body = match parts.extensions.get::().copied() { - Some(BodyLimitKind::Disable) => body, - Some(BodyLimitKind::Limit(limit)) => { - crate::body::Body::from_body(http_body_util::Limited::new(body, limit)) - } - None => body, - }; - let boundary = multer::parse_boundary( parts .headers @@ -259,9 +245,7 @@ mod multipart_tests { }; fn _test_compile() { - async fn handler(_: Multipart<'_>) -> Result<(), Infallible> { - Ok(()) - } + async fn handler(_: Multipart<'_>) {} let app = test_helpers::to_service(handler); let addr = Address::Ip(SocketAddr::new( IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), @@ -272,17 +256,17 @@ mod multipart_tests { async fn run_handler(service: S, port: u16) where - S: Service - + Send - + Sync - + 'static, + S: Service + + Send + + Sync + + 'static, { let addr = Address::Ip(SocketAddr::new( IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), port, )); - tokio::spawn(Server::new(service).run(addr.clone())); + tokio::spawn(Server::new(service).run(addr)); tokio::time::sleep(std::time::Duration::from_secs(1)).await; } From a2c6ce6ee097a8175677d7f89425255678c0c9f2 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Tue, 22 Oct 2024 21:01:41 +0800 Subject: [PATCH 06/22] feat(http): add multipart for server --- volo-http/src/server/layer/body_limit.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/volo-http/src/server/layer/body_limit.rs b/volo-http/src/server/layer/body_limit.rs index b1921a29..3dac131d 100644 --- a/volo-http/src/server/layer/body_limit.rs +++ b/volo-http/src/server/layer/body_limit.rs @@ -16,7 +16,11 @@ pub(crate) enum BodyLimitKind { /// [`Layer`] for limiting body size /// -/// Currently only supports [`Multipart`](crate::server::utils::multipart::Multipart) extractor. +/// Get the body size by the priority: +/// +/// 1. [`http::header::CONTENT_LENGTH`] +/// +/// 2. [`http_body::Body::size_hint()`] /// /// See [`BodyLimitLayer::max`] for more details. #[derive(Clone)] @@ -111,6 +115,7 @@ where } } } + let mut req = ServerRequest::from_parts(parts, body); req.extensions_mut().insert(self.kind); Ok(self.service.call(cx, req).await?.into_response()) From b4c7642c2500161a44a57caee42ce994348542c6 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Tue, 22 Oct 2024 21:02:09 +0800 Subject: [PATCH 07/22] feat(http): add multipart for server --- volo-http/src/server/layer/body_limit.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/volo-http/src/server/layer/body_limit.rs b/volo-http/src/server/layer/body_limit.rs index 3dac131d..3d339a87 100644 --- a/volo-http/src/server/layer/body_limit.rs +++ b/volo-http/src/server/layer/body_limit.rs @@ -117,7 +117,6 @@ where } let mut req = ServerRequest::from_parts(parts, body); - req.extensions_mut().insert(self.kind); Ok(self.service.call(cx, req).await?.into_response()) } } From e57c052e48631c023f333df39c18968bc5879db0 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Tue, 22 Oct 2024 21:02:18 +0800 Subject: [PATCH 08/22] feat(http): add multipart for server --- volo-http/src/server/layer/body_limit.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volo-http/src/server/layer/body_limit.rs b/volo-http/src/server/layer/body_limit.rs index 3d339a87..c32074d0 100644 --- a/volo-http/src/server/layer/body_limit.rs +++ b/volo-http/src/server/layer/body_limit.rs @@ -116,7 +116,7 @@ where } } - let mut req = ServerRequest::from_parts(parts, body); + let req = ServerRequest::from_parts(parts, body); Ok(self.service.call(cx, req).await?.into_response()) } } From 427fd201179330b171cf92461b5af1ed14eeebda Mon Sep 17 00:00:00 2001 From: StellarisW Date: Tue, 22 Oct 2024 21:06:45 +0800 Subject: [PATCH 09/22] feat(http): add multipart for server --- volo-http/src/server/layer/body_limit.rs | 15 +++++++++------ volo-http/src/server/utils/multipart.rs | 8 ++++---- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/volo-http/src/server/layer/body_limit.rs b/volo-http/src/server/layer/body_limit.rs index c32074d0..e34d110d 100644 --- a/volo-http/src/server/layer/body_limit.rs +++ b/volo-http/src/server/layer/body_limit.rs @@ -2,9 +2,9 @@ use http::StatusCode; use http_body::Body; use motore::{layer::Layer, Service}; -use crate::{context::ServerContext, request::ServerRequest}; -use crate::response::ServerResponse; -use crate::server::IntoResponse; +use crate::{ + context::ServerContext, request::ServerRequest, response::ServerResponse, server::IntoResponse, +}; #[derive(Debug, Clone, Copy)] pub(crate) enum BodyLimitKind { @@ -67,8 +67,7 @@ impl BodyLimitLayer { } } -impl Layer for BodyLimitLayer -{ +impl Layer for BodyLimitLayer { type Service = BodyLimitService; fn layer(self, inner: S) -> Self::Service { @@ -104,7 +103,11 @@ where let (parts, body) = req.into_parts(); if let BodyLimitKind::Block(limit) = self.kind { // get body size from content length - if let Some(size) = parts.headers.get(http::header::CONTENT_LENGTH).and_then(|v| v.to_str().ok().and_then(|s| s.parse::().ok())) { + if let Some(size) = parts + .headers + .get(http::header::CONTENT_LENGTH) + .and_then(|v| v.to_str().ok().and_then(|s| s.parse::().ok())) + { if size > limit { return Ok(StatusCode::PAYLOAD_TOO_LARGE.into_response()); } diff --git a/volo-http/src/server/utils/multipart.rs b/volo-http/src/server/utils/multipart.rs index 357021a6..d861f36e 100644 --- a/volo-http/src/server/utils/multipart.rs +++ b/volo-http/src/server/utils/multipart.rs @@ -256,10 +256,10 @@ mod multipart_tests { async fn run_handler(service: S, port: u16) where - S: Service - + Send - + Sync - + 'static, + S: Service + + Send + + Sync + + 'static, { let addr = Address::Ip(SocketAddr::new( IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), From b2a3a3de429cc4bfd48d1e60035cc220d7b8ea05 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Wed, 23 Oct 2024 17:49:08 +0800 Subject: [PATCH 10/22] feat(http): add multipart for server --- Cargo.toml | 2 +- volo-http/src/server/extract.rs | 1 - volo-http/src/server/layer/body_limit.rs | 68 +++++++----------------- volo-http/src/server/utils/multipart.rs | 55 ++++++++----------- 4 files changed, 43 insertions(+), 83 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 36f3e518..2dce3fd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -123,7 +123,7 @@ tower = "0.5" tracing = "0.1" tracing-subscriber = "0.3" update-informer = "1" -url="2.5.2" +url = "2.5.2" url_path = "0.1" walkdir = "2" diff --git a/volo-http/src/server/extract.rs b/volo-http/src/server/extract.rs index 5dc62a7e..e054939a 100644 --- a/volo-http/src/server/extract.rs +++ b/volo-http/src/server/extract.rs @@ -408,7 +408,6 @@ where parts: Parts, body: B, ) -> Result { - // TODO: add limited body let bytes = body .collect() .await diff --git a/volo-http/src/server/layer/body_limit.rs b/volo-http/src/server/layer/body_limit.rs index e34d110d..ab327204 100644 --- a/volo-http/src/server/layer/body_limit.rs +++ b/volo-http/src/server/layer/body_limit.rs @@ -6,26 +6,12 @@ use crate::{ context::ServerContext, request::ServerRequest, response::ServerResponse, server::IntoResponse, }; -#[derive(Debug, Clone, Copy)] -pub(crate) enum BodyLimitKind { - #[allow(dead_code)] - Disable, - #[allow(dead_code)] - Block(usize), -} - /// [`Layer`] for limiting body size /// -/// Get the body size by the priority: -/// -/// 1. [`http::header::CONTENT_LENGTH`] -/// -/// 2. [`http_body::Body::size_hint()`] -/// -/// See [`BodyLimitLayer::max`] for more details. +/// See [`BodyLimitLayer::new`] for more details. #[derive(Clone)] pub struct BodyLimitLayer { - kind: BodyLimitKind, + limit: usize, } impl BodyLimitLayer { @@ -48,22 +34,10 @@ impl BodyLimitLayer { /// /// let router: Router = Router::new() /// .route("/", post(handler)) - /// .layer(BodyLimitLayer::max(1024)); // limit body size to 1KB + /// .layer(BodyLimitLayer::new(1024)); // limit body size to 1KB /// ``` - pub fn max(body_limit: usize) -> Self { - Self { - kind: BodyLimitKind::Block(body_limit), - } - } - - /// Create a new [`BodyLimitLayer`] with `body_limit` disabled. - /// - /// It's unnecessary to use this method, because the `body_limit` is disabled by default. - #[allow(dead_code)] - fn disable() -> Self { - Self { - kind: BodyLimitKind::Disable, - } + pub fn new(body_limit: usize) -> Self { + Self { limit: body_limit } } } @@ -73,7 +47,7 @@ impl Layer for BodyLimitLayer { fn layer(self, inner: S) -> Self::Service { BodyLimitService { service: inner, - kind: self.kind, + limit: self.limit, } } } @@ -83,7 +57,7 @@ impl Layer for BodyLimitLayer { /// See [`BodyLimitLayer`] for more details. pub struct BodyLimitService { service: S, - kind: BodyLimitKind, + limit: usize, } impl Service for BodyLimitService @@ -101,21 +75,19 @@ where req: ServerRequest, ) -> Result { let (parts, body) = req.into_parts(); - if let BodyLimitKind::Block(limit) = self.kind { - // get body size from content length - if let Some(size) = parts - .headers - .get(http::header::CONTENT_LENGTH) - .and_then(|v| v.to_str().ok().and_then(|s| s.parse::().ok())) - { - if size > limit { - return Ok(StatusCode::PAYLOAD_TOO_LARGE.into_response()); - } - } else { - // get body size from stream - if body.size_hint().lower() > limit as u64 { - return Ok(StatusCode::PAYLOAD_TOO_LARGE.into_response()); - } + // get body size from content length + if let Some(size) = parts + .headers + .get(http::header::CONTENT_LENGTH) + .and_then(|v| v.to_str().ok().and_then(|s| s.parse::().ok())) + { + if size > self.limit { + return Ok(StatusCode::PAYLOAD_TOO_LARGE.into_response()); + } + } else { + // get body size from stream + if body.size_hint().lower() > self.limit as u64 { + return Ok(StatusCode::PAYLOAD_TOO_LARGE.into_response()); } } diff --git a/volo-http/src/server/utils/multipart.rs b/volo-http/src/server/utils/multipart.rs index d861f36e..f9da2d5c 100644 --- a/volo-http/src/server/utils/multipart.rs +++ b/volo-http/src/server/utils/multipart.rs @@ -5,7 +5,7 @@ //! //! # Example //! -//! ```rust,no_run +//! ```rust //! use http::StatusCode; //! use volo_http::{ //! response::ServerResponse, @@ -57,7 +57,7 @@ use crate::{ /// /// # Example /// -/// ```rust,no_run +/// ```rust /// use http::StatusCode; /// use volo_http::{ /// response::ServerResponse, @@ -80,7 +80,7 @@ use crate::{ /// Since the body is unlimited, so it is recommended to use /// [`BodyLimitLayer`](crate::server::layer::BodyLimitLayer) to limit the size of the body. /// -/// ```rust,no_run +/// ```rust /// use http::StatusCode; /// use volo_http::{ /// Router, @@ -91,25 +91,25 @@ use crate::{ /// } /// }; /// -/// # async fn upload_handler(mut multipart: Multipart<'static>) -> Result { +/// # async fn upload_handler(mut multipart: Multipart) -> Result { /// # Ok(StatusCode::OK) /// # } /// /// let app: Router<_>= Router::new() /// .route("/",post(upload_handler)) -/// .layer( BodyLimitLayer::max(1024)); +/// .layer( BodyLimitLayer::new(1024)); /// ``` #[must_use] -pub struct Multipart<'r> { - inner: multer::Multipart<'r>, +pub struct Multipart { + inner: multer::Multipart<'static>, } -impl<'r> Multipart<'r> { +impl Multipart { /// Iterate over all [`Field`] in [`Multipart`] /// /// # Example /// - /// ```rust,no_run + /// ```rust /// # use volo_http::server::utils::multipart::Multipart; /// # let mut multipart: Multipart; /// // Extract each field from multipart by using while loop @@ -120,12 +120,12 @@ impl<'r> Multipart<'r> { /// } /// # } /// ``` - pub async fn next_field(&mut self) -> Result>, MultipartRejectionError> { + pub async fn next_field(&mut self) -> Result>, MultipartRejectionError> { Ok(self.inner.next_field().await?) } } -impl<'r> FromRequest for Multipart<'r> { +impl FromRequest for Multipart { type Rejection = MultipartRejectionError; async fn from_request( _: &mut ServerContext, @@ -136,8 +136,9 @@ impl<'r> FromRequest for Multipart<'r> { parts .headers .get(http::header::CONTENT_TYPE) - .map(|h| h.to_str().unwrap_or_default()) - .unwrap_or_default(), + .ok_or(multer::Error::NoMultipart)? + .to_str() + .map_err(|_| multer::Error::NoBoundary)?, )?; let multipart = multer::Multipart::new(body.into_data_stream(), boundary); @@ -175,20 +176,7 @@ fn status_code_from_multer_error(err: &multer::Error) -> StatusCode { multer::Error::FieldSizeExceeded { .. } | multer::Error::StreamSizeExceeded { .. } => { StatusCode::PAYLOAD_TOO_LARGE } - multer::Error::StreamReadFailed(err) => { - if let Some(err) = err.downcast_ref::() { - return status_code_from_multer_error(err); - } - - if err - .downcast_ref::() - .is_some() - { - return StatusCode::PAYLOAD_TOO_LARGE; - } - - StatusCode::INTERNAL_SERVER_ERROR - } + multer::Error::StreamReadFailed(_) => StatusCode::INTERNAL_SERVER_ERROR, _ => StatusCode::INTERNAL_SERVER_ERROR, } } @@ -214,7 +202,7 @@ impl fmt::Display for MultipartRejectionError { impl IntoResponse for MultipartRejectionError { fn into_response(self) -> http::Response { - (self.to_status_code(), self.to_string()).into_response() + self.to_status_code().into_response() } } @@ -245,7 +233,7 @@ mod multipart_tests { }; fn _test_compile() { - async fn handler(_: Multipart<'_>) {} + async fn handler(_: Multipart) {} let app = test_helpers::to_service(handler); let addr = Address::Ip(SocketAddr::new( IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), @@ -270,13 +258,14 @@ mod multipart_tests { tokio::time::sleep(std::time::Duration::from_secs(1)).await; } + #[tokio::test] async fn test_single_field_upload() { const BYTES: &[u8] = "🦀".as_bytes(); const FILE_NAME: &str = "index.html"; const CONTENT_TYPE: &str = "text/html; charset=utf-8"; - async fn handler(mut multipart: Multipart<'static>) -> impl IntoResponse { + async fn handler(mut multipart: Multipart) -> impl IntoResponse { let field = multipart.next_field().await.unwrap().unwrap(); assert_eq!(field.file_name().unwrap(), FILE_NAME); @@ -322,7 +311,7 @@ mod multipart_tests { const FILE_NAME1: &str = "index1.html"; const FILE_NAME2: &str = "index2.html"; - async fn handler(mut multipart: Multipart<'static>) -> Result<(), MultipartRejectionError> { + async fn handler(mut multipart: Multipart) -> Result<(), MultipartRejectionError> { while let Some(field) = multipart.next_field().await? { match field.name() { Some(FIELD_NAME1) => { @@ -382,7 +371,7 @@ mod multipart_tests { #[tokio::test] async fn test_large_field_upload() { - async fn handler(mut multipart: Multipart<'static>) -> Result<(), MultipartRejectionError> { + async fn handler(mut multipart: Multipart) -> Result<(), MultipartRejectionError> { while let Some(field) = multipart.next_field().await? { field.bytes().await?; } @@ -410,7 +399,7 @@ mod multipart_tests { let app: Router<_> = Router::new() .route("/", post(handler)) - .layer(BodyLimitLayer::max(1024)); + .layer(BodyLimitLayer::new(1024)); run_handler(app, 8003).await; From 8c28eeaac568b0b3f15d96434040b668fad74f05 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Wed, 23 Oct 2024 17:52:21 +0800 Subject: [PATCH 11/22] feat(http): add multipart for server --- volo-http/src/server/utils/multipart.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volo-http/src/server/utils/multipart.rs b/volo-http/src/server/utils/multipart.rs index f9da2d5c..432d6f1a 100644 --- a/volo-http/src/server/utils/multipart.rs +++ b/volo-http/src/server/utils/multipart.rs @@ -65,7 +65,7 @@ use crate::{ /// }; /// /// async fn upload( -/// mut multipart: Multipart<'static>, +/// mut multipart: Multipart, /// ) -> Result { /// while let Some(field) = multipart.next_field().await? { /// todo!() From 3eaad8492287a13de424a881d0743f386f25fe8e Mon Sep 17 00:00:00 2001 From: StellarisW Date: Wed, 23 Oct 2024 17:52:53 +0800 Subject: [PATCH 12/22] feat(http): add multipart for server --- volo-http/src/server/utils/multipart.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volo-http/src/server/utils/multipart.rs b/volo-http/src/server/utils/multipart.rs index 432d6f1a..e0bba9bc 100644 --- a/volo-http/src/server/utils/multipart.rs +++ b/volo-http/src/server/utils/multipart.rs @@ -17,7 +17,7 @@ //! }; //! //! async fn upload( -//! mut multipart: Multipart<'static>, +//! mut multipart: Multipart, //! ) -> Result { //! while let Some(field) = multipart.next_field().await? { //! let name = field.name().unwrap(); From f15936f045c6175e6728e691b17d6a99c60a173b Mon Sep 17 00:00:00 2001 From: StellarisW Date: Wed, 23 Oct 2024 17:54:28 +0800 Subject: [PATCH 13/22] feat(http): add multipart for server --- volo-http/src/server/utils/multipart.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volo-http/src/server/utils/multipart.rs b/volo-http/src/server/utils/multipart.rs index e0bba9bc..49aa12d2 100644 --- a/volo-http/src/server/utils/multipart.rs +++ b/volo-http/src/server/utils/multipart.rs @@ -97,7 +97,7 @@ use crate::{ /// /// let app: Router<_>= Router::new() /// .route("/",post(upload_handler)) -/// .layer( BodyLimitLayer::new(1024)); +/// .layer(BodyLimitLayer::new(1024)); /// ``` #[must_use] pub struct Multipart { From 11c33670035791c61d429130ece2b63a12e90cb4 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Thu, 24 Oct 2024 17:31:24 +0800 Subject: [PATCH 14/22] feat(http): add multipart for server --- volo-http/src/server/layer/body_limit.rs | 46 +++++++++++++++++++++-- volo-http/src/server/utils/multipart.rs | 48 +----------------------- 2 files changed, 44 insertions(+), 50 deletions(-) diff --git a/volo-http/src/server/layer/body_limit.rs b/volo-http/src/server/layer/body_limit.rs index ab327204..9590b5e5 100644 --- a/volo-http/src/server/layer/body_limit.rs +++ b/volo-http/src/server/layer/body_limit.rs @@ -60,11 +60,12 @@ pub struct BodyLimitService { limit: usize, } -impl Service for BodyLimitService +impl Service> for BodyLimitService where - S: Service + Send + Sync + 'static, + S: Service> + Send + Sync + 'static, S::Response: IntoResponse, S::Error: IntoResponse, + B: Body + Send, { type Response = ServerResponse; type Error = S::Error; @@ -72,7 +73,7 @@ where async fn call( &self, cx: &mut ServerContext, - req: ServerRequest, + req: ServerRequest, ) -> Result { let (parts, body) = req.into_parts(); // get body size from content length @@ -95,3 +96,42 @@ where Ok(self.service.call(cx, req).await?.into_response()) } } + +#[cfg(test)] +mod tests { + use http::{Method, StatusCode}; + use motore::layer::Layer; + use motore::Service; + use rand::Rng; + use crate::server::layer::BodyLimitLayer; + use crate::server::route::{any, Route}; + use crate::server::test_helpers::empty_cx; + use crate::utils::test_helpers::simple_req; + + #[tokio::test] + async fn test_body_limit() { + async fn handler() -> &'static str { + "Hello, World" + } + + let body_limit_layer = BodyLimitLayer::new(1024); + let route: Route<_> = Route::new(any(handler)); + let service = body_limit_layer.layer(route); + + let mut cx = empty_cx(); + + // Test case 1: reject + let mut rng = rand::thread_rng(); + let min_part_size = 4096; + let mut body: Vec = vec![0; min_part_size]; + rng.fill(&mut body[..]); + let req = simple_req(Method::GET, "/", unsafe { String::from_utf8_unchecked(body) }); + let res = service.call(&mut cx, req).await.unwrap(); + assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE); + + // Test case 2: not reject + let req = simple_req(Method::GET, "/", "Hello, World".to_string()); + let res = service.call(&mut cx, req).await.unwrap(); + assert_eq!(res.status(), StatusCode::OK); + } +} \ No newline at end of file diff --git a/volo-http/src/server/utils/multipart.rs b/volo-http/src/server/utils/multipart.rs index 49aa12d2..0523d537 100644 --- a/volo-http/src/server/utils/multipart.rs +++ b/volo-http/src/server/utils/multipart.rs @@ -294,7 +294,7 @@ mod multipart_tests { let url = url::Url::parse(url_str.as_str()).unwrap(); reqwest::Client::new() - .post(url.clone()) + .post(url) .multipart(form) .send() .await @@ -368,50 +368,4 @@ mod multipart_tests { .unwrap(); } } - - #[tokio::test] - async fn test_large_field_upload() { - async fn handler(mut multipart: Multipart) -> Result<(), MultipartRejectionError> { - while let Some(field) = multipart.next_field().await? { - field.bytes().await?; - } - - Ok(()) - } - - // generate random bytes - let mut rng = rand::thread_rng(); - let min_part_size = 4096; - let mut body = vec![0; min_part_size]; - rng.fill(&mut body[..]); - - let content_type = "text/html; charset=utf-8"; - let field_name = "file"; - let file_name = "index.html"; - - let form = Form::new().part( - field_name, - reqwest::multipart::Part::bytes(body) - .file_name(file_name) - .mime_str(content_type) - .unwrap(), - ); - - let app: Router<_> = Router::new() - .route("/", post(handler)) - .layer(BodyLimitLayer::new(1024)); - - run_handler(app, 8003).await; - - let url_str = format!("http://127.0.0.1:{}", 8003); - let url = url::Url::parse(url_str.as_str()).unwrap(); - - let resp = reqwest::Client::new() - .post(url.clone()) - .multipart(form) - .send() - .await - .unwrap(); - assert_eq!(resp.status(), http::StatusCode::PAYLOAD_TOO_LARGE); - } } From 865976badbd1e452e4aa6dbfcec70a5139bf4846 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Thu, 24 Oct 2024 17:33:53 +0800 Subject: [PATCH 15/22] feat(http): add multipart for server --- volo-http/Cargo.toml | 3 +-- volo-http/src/server/layer/body_limit.rs | 24 +++++++++++++++--------- volo-http/src/server/utils/multipart.rs | 8 ++------ 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/volo-http/Cargo.toml b/volo-http/Cargo.toml index 0a1bae4d..857d0e8c 100644 --- a/volo-http/Cargo.toml +++ b/volo-http/Cargo.toml @@ -60,7 +60,6 @@ tokio-util = { workspace = true, features = ["io"] } tracing.workspace = true # =====optional===== - multer = { workspace = true, optional = true } # server optional @@ -86,9 +85,9 @@ sonic-rs = { workspace = true, optional = true } async-stream.workspace = true libc.workspace = true serde = { workspace = true, features = ["derive"] } -tokio-test.workspace = true rand.workspace = true reqwest = { workspace = true, features = ["multipart"] } +tokio-test.workspace = true url.workspace = true [features] diff --git a/volo-http/src/server/layer/body_limit.rs b/volo-http/src/server/layer/body_limit.rs index 9590b5e5..bc7c4b99 100644 --- a/volo-http/src/server/layer/body_limit.rs +++ b/volo-http/src/server/layer/body_limit.rs @@ -60,7 +60,7 @@ pub struct BodyLimitService { limit: usize, } -impl Service> for BodyLimitService +impl Service> for BodyLimitService where S: Service> + Send + Sync + 'static, S::Response: IntoResponse, @@ -100,13 +100,17 @@ where #[cfg(test)] mod tests { use http::{Method, StatusCode}; - use motore::layer::Layer; - use motore::Service; + use motore::{layer::Layer, Service}; use rand::Rng; - use crate::server::layer::BodyLimitLayer; - use crate::server::route::{any, Route}; - use crate::server::test_helpers::empty_cx; - use crate::utils::test_helpers::simple_req; + + use crate::{ + server::{ + layer::BodyLimitLayer, + route::{any, Route}, + test_helpers::empty_cx, + }, + utils::test_helpers::simple_req, + }; #[tokio::test] async fn test_body_limit() { @@ -125,7 +129,9 @@ mod tests { let min_part_size = 4096; let mut body: Vec = vec![0; min_part_size]; rng.fill(&mut body[..]); - let req = simple_req(Method::GET, "/", unsafe { String::from_utf8_unchecked(body) }); + let req = simple_req(Method::GET, "/", unsafe { + String::from_utf8_unchecked(body) + }); let res = service.call(&mut cx, req).await.unwrap(); assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE); @@ -134,4 +140,4 @@ mod tests { let res = service.call(&mut cx, req).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); } -} \ No newline at end of file +} diff --git a/volo-http/src/server/utils/multipart.rs b/volo-http/src/server/utils/multipart.rs index 0523d537..596d6f02 100644 --- a/volo-http/src/server/utils/multipart.rs +++ b/volo-http/src/server/utils/multipart.rs @@ -16,9 +16,7 @@ //! Router, //! }; //! -//! async fn upload( -//! mut multipart: Multipart, -//! ) -> Result { +//! async fn upload(mut multipart: Multipart) -> Result { //! while let Some(field) = multipart.next_field().await? { //! let name = field.name().unwrap(); //! let value = field.bytes().await?; @@ -64,9 +62,7 @@ use crate::{ /// server::utils::multipart::{Multipart, MultipartRejectionError}, /// }; /// -/// async fn upload( -/// mut multipart: Multipart, -/// ) -> Result { +/// async fn upload(mut multipart: Multipart) -> Result { /// while let Some(field) = multipart.next_field().await? { /// todo!() /// } From 74e6b241c8f0ed0b78f12ee2fcb037f8f0dd829b Mon Sep 17 00:00:00 2001 From: StellarisW Date: Thu, 24 Oct 2024 17:34:42 +0800 Subject: [PATCH 16/22] feat(http): add multipart for server --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2dce3fd7..23b1b4fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,7 +80,7 @@ mime = "0.3" mime_guess = { version = "2", default-features = false } mockall = "0.13" mockall_double = "0.3" -multer = "3.1.0" +multer = "3.1" mur3 = "0.1" nix = "0.29" nom = "7" @@ -97,7 +97,7 @@ proc-macro2 = "1" quote = "1" rand = "0.8" regex = "1" -reqwest = "0.12.8" +reqwest = "0.12" run_script = "0.10" rustc-hash = { version = "2", features = ["rand"] } same-file = "1" @@ -123,7 +123,7 @@ tower = "0.5" tracing = "0.1" tracing-subscriber = "0.3" update-informer = "1" -url = "2.5.2" +url = "2.5" url_path = "0.1" walkdir = "2" From 6bcb033eded62fef86dfcf72ddf2b53c9fd2c9fc Mon Sep 17 00:00:00 2001 From: StellarisW Date: Fri, 25 Oct 2024 12:01:30 +0800 Subject: [PATCH 17/22] feat(http): add multipart for server --- volo-http/src/server/utils/multipart.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/volo-http/src/server/utils/multipart.rs b/volo-http/src/server/utils/multipart.rs index 596d6f02..5501ee4d 100644 --- a/volo-http/src/server/utils/multipart.rs +++ b/volo-http/src/server/utils/multipart.rs @@ -233,7 +233,7 @@ mod multipart_tests { let app = test_helpers::to_service(handler); let addr = Address::Ip(SocketAddr::new( IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), - 8001, + 25241, )); let _server = Server::new(app).run(addr); } @@ -284,9 +284,9 @@ mod multipart_tests { )])), ); - run_handler(test_helpers::to_service(handler), 8001).await; + run_handler(test_helpers::to_service(handler), 25241).await; - let url_str = format!("http://127.0.0.1:{}", 8001); + let url_str = format!("http://127.0.0.1:{}", 25241); let url = url::Url::parse(url_str.as_str()).unwrap(); reqwest::Client::new() @@ -350,9 +350,9 @@ mod multipart_tests { )])), ); - run_handler(test_helpers::to_service(handler), 8002).await; + run_handler(test_helpers::to_service(handler), 25242).await; - let url_str = format!("http://127.0.0.1:{}", 8002); + let url_str = format!("http://127.0.0.1:{}", 25242); let url = url::Url::parse(url_str.as_str()).unwrap(); for form in vec![form1, form2] { From 94b27efa117bb318238e5c27752684c77a763679 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Fri, 25 Oct 2024 12:49:57 +0800 Subject: [PATCH 18/22] feat(http): add multipart for server --- volo-http/src/server/utils/multipart.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/volo-http/src/server/utils/multipart.rs b/volo-http/src/server/utils/multipart.rs index 5501ee4d..dd00a893 100644 --- a/volo-http/src/server/utils/multipart.rs +++ b/volo-http/src/server/utils/multipart.rs @@ -18,7 +18,7 @@ //! //! async fn upload(mut multipart: Multipart) -> Result { //! while let Some(field) = multipart.next_field().await? { -//! let name = field.name().unwrap(); +//! let name = field.name().unwrap().to_string(); //! let value = field.bytes().await?; //! //! println!("The field {} has {} bytes", name, value.len()); @@ -109,10 +109,10 @@ impl Multipart { /// # use volo_http::server::utils::multipart::Multipart; /// # let mut multipart: Multipart; /// // Extract each field from multipart by using while loop - /// # async { - /// while let Some(field) = multipart.next_field().await? { + /// # async fn upload(mut multipart: Multipart) { + /// while let Some(field) = multipart.next_field().await.unwrap() { /// let name = field.name().unwrap().to_string(); // Get field name - /// let data = field.bytes().await?; // Get field data + /// let data = field.bytes().await.unwrap(); // Get field data /// } /// # } /// ``` @@ -210,7 +210,6 @@ mod multipart_tests { }; use motore::Service; - use rand::Rng; use reqwest::multipart::Form; use volo::net::Address; @@ -219,13 +218,11 @@ mod multipart_tests { request::ServerRequest, response::ServerResponse, server::{ - layer::BodyLimitLayer, - route::post, test_helpers, utils::multipart::{Multipart, MultipartRejectionError}, IntoResponse, }, - Router, Server, + Server, }; fn _test_compile() { From f0351e61a494fc6d828f00a95a5234d05bc19696 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Fri, 25 Oct 2024 14:35:15 +0800 Subject: [PATCH 19/22] feat(http): add multipart for server --- Cargo.lock | 1 - volo-http/Cargo.toml | 1 - volo-http/src/server/layer/body_limit.rs | 31 +++--------------------- volo-http/src/server/utils/multipart.rs | 27 +++++++++------------ 4 files changed, 15 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e8349c72..744cc44b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3854,7 +3854,6 @@ dependencies = [ "parking_lot 0.12.3", "paste", "pin-project", - "rand", "reqwest 0.12.8", "scopeguard", "serde", diff --git a/volo-http/Cargo.toml b/volo-http/Cargo.toml index 857d0e8c..9ed50dfa 100644 --- a/volo-http/Cargo.toml +++ b/volo-http/Cargo.toml @@ -85,7 +85,6 @@ sonic-rs = { workspace = true, optional = true } async-stream.workspace = true libc.workspace = true serde = { workspace = true, features = ["derive"] } -rand.workspace = true reqwest = { workspace = true, features = ["multipart"] } tokio-test.workspace = true url.workspace = true diff --git a/volo-http/src/server/layer/body_limit.rs b/volo-http/src/server/layer/body_limit.rs index bc7c4b99..24c2778e 100644 --- a/volo-http/src/server/layer/body_limit.rs +++ b/volo-http/src/server/layer/body_limit.rs @@ -18,23 +18,6 @@ impl BodyLimitLayer { /// Create a new [`BodyLimitLayer`] with given `body_limit`. /// /// If the Body is larger than the `body_limit`, the request will be rejected. - /// - /// # Examples - /// - /// ``` - /// use http::StatusCode; - /// use volo_http::server::{ - /// layer::BodyLimitLayer, - /// route::{post, Router}, - /// }; - /// - /// async fn handler() -> &'static str { - /// "Hello, World" - /// } - /// - /// let router: Router = Router::new() - /// .route("/", post(handler)) - /// .layer(BodyLimitLayer::new(1024)); // limit body size to 1KB /// ``` pub fn new(body_limit: usize) -> Self { Self { limit: body_limit } @@ -64,7 +47,6 @@ impl Service> for BodyLimitService where S: Service> + Send + Sync + 'static, S::Response: IntoResponse, - S::Error: IntoResponse, B: Body + Send, { type Response = ServerResponse; @@ -101,7 +83,6 @@ where mod tests { use http::{Method, StatusCode}; use motore::{layer::Layer, Service}; - use rand::Rng; use crate::{ server::{ @@ -118,25 +99,19 @@ mod tests { "Hello, World" } - let body_limit_layer = BodyLimitLayer::new(1024); + let body_limit_layer = BodyLimitLayer::new(8); let route: Route<_> = Route::new(any(handler)); let service = body_limit_layer.layer(route); let mut cx = empty_cx(); // Test case 1: reject - let mut rng = rand::thread_rng(); - let min_part_size = 4096; - let mut body: Vec = vec![0; min_part_size]; - rng.fill(&mut body[..]); - let req = simple_req(Method::GET, "/", unsafe { - String::from_utf8_unchecked(body) - }); + let req = simple_req(Method::GET, "/", "111111111".to_string()); let res = service.call(&mut cx, req).await.unwrap(); assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE); // Test case 2: not reject - let req = simple_req(Method::GET, "/", "Hello, World".to_string()); + let req = simple_req(Method::GET, "/", "1".to_string()); let res = service.call(&mut cx, req).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); } diff --git a/volo-http/src/server/utils/multipart.rs b/volo-http/src/server/utils/multipart.rs index dd00a893..c7abee8d 100644 --- a/volo-http/src/server/utils/multipart.rs +++ b/volo-http/src/server/utils/multipart.rs @@ -237,10 +237,10 @@ mod multipart_tests { async fn run_handler(service: S, port: u16) where - S: Service - + Send - + Sync - + 'static, + S: Service + + Send + + Sync + + 'static, { let addr = Address::Ip(SocketAddr::new( IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), @@ -324,7 +324,7 @@ mod multipart_tests { Ok(()) } - let form1 = Form::new().part( + let form = Form::new().part( FIELD_NAME1, reqwest::multipart::Part::bytes(BYTES) .file_name(FILE_NAME1) @@ -334,8 +334,7 @@ mod multipart_tests { reqwest::header::HeaderName::from_static("foo1"), reqwest::header::HeaderValue::from_static("bar1"), )])), - ); - let form2 = Form::new().part( + ).part( FIELD_NAME2, reqwest::multipart::Part::bytes(BYTES) .file_name(FILE_NAME2) @@ -352,13 +351,11 @@ mod multipart_tests { let url_str = format!("http://127.0.0.1:{}", 25242); let url = url::Url::parse(url_str.as_str()).unwrap(); - for form in vec![form1, form2] { - reqwest::Client::new() - .post(url.clone()) - .multipart(form) - .send() - .await - .unwrap(); - } + reqwest::Client::new() + .post(url.clone()) + .multipart(form) + .send() + .await + .unwrap(); } } From 45193d482bf2458485c2f9f9220b0c0853a842ad Mon Sep 17 00:00:00 2001 From: StellarisW Date: Fri, 25 Oct 2024 14:35:25 +0800 Subject: [PATCH 20/22] feat(http): add multipart for server --- volo-http/src/server/utils/multipart.rs | 52 +++++++++++++------------ 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/volo-http/src/server/utils/multipart.rs b/volo-http/src/server/utils/multipart.rs index c7abee8d..c04e2a81 100644 --- a/volo-http/src/server/utils/multipart.rs +++ b/volo-http/src/server/utils/multipart.rs @@ -237,10 +237,10 @@ mod multipart_tests { async fn run_handler(service: S, port: u16) where - S: Service - + Send - + Sync - + 'static, + S: Service + + Send + + Sync + + 'static, { let addr = Address::Ip(SocketAddr::new( IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), @@ -324,27 +324,29 @@ mod multipart_tests { Ok(()) } - let form = Form::new().part( - FIELD_NAME1, - reqwest::multipart::Part::bytes(BYTES) - .file_name(FILE_NAME1) - .mime_str(CONTENT_TYPE) - .unwrap() - .headers(reqwest::header::HeaderMap::from_iter([( - reqwest::header::HeaderName::from_static("foo1"), - reqwest::header::HeaderValue::from_static("bar1"), - )])), - ).part( - FIELD_NAME2, - reqwest::multipart::Part::bytes(BYTES) - .file_name(FILE_NAME2) - .mime_str(CONTENT_TYPE) - .unwrap() - .headers(reqwest::header::HeaderMap::from_iter([( - reqwest::header::HeaderName::from_static("foo2"), - reqwest::header::HeaderValue::from_static("bar2"), - )])), - ); + let form = Form::new() + .part( + FIELD_NAME1, + reqwest::multipart::Part::bytes(BYTES) + .file_name(FILE_NAME1) + .mime_str(CONTENT_TYPE) + .unwrap() + .headers(reqwest::header::HeaderMap::from_iter([( + reqwest::header::HeaderName::from_static("foo1"), + reqwest::header::HeaderValue::from_static("bar1"), + )])), + ) + .part( + FIELD_NAME2, + reqwest::multipart::Part::bytes(BYTES) + .file_name(FILE_NAME2) + .mime_str(CONTENT_TYPE) + .unwrap() + .headers(reqwest::header::HeaderMap::from_iter([( + reqwest::header::HeaderName::from_static("foo2"), + reqwest::header::HeaderValue::from_static("bar2"), + )])), + ); run_handler(test_helpers::to_service(handler), 25242).await; From c56beae6547de9067ddee71226357504c8bdf1b3 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Fri, 25 Oct 2024 14:44:50 +0800 Subject: [PATCH 21/22] feat(http): add multipart for server --- volo-http/src/server/utils/multipart.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/volo-http/src/server/utils/multipart.rs b/volo-http/src/server/utils/multipart.rs index c04e2a81..47c4091e 100644 --- a/volo-http/src/server/utils/multipart.rs +++ b/volo-http/src/server/utils/multipart.rs @@ -87,9 +87,9 @@ use crate::{ /// } /// }; /// -/// # async fn upload_handler(mut multipart: Multipart) -> Result { -/// # Ok(StatusCode::OK) -/// # } +/// async fn upload_handler(mut multipart: Multipart) -> Result { +/// Ok(StatusCode::OK) +/// } /// /// let app: Router<_>= Router::new() /// .route("/",post(upload_handler)) From 6721517c2c381b1e22e4e7973e52c5f4efe1fbf9 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Fri, 25 Oct 2024 14:49:47 +0800 Subject: [PATCH 22/22] feat(http): add multipart for server --- volo-http/src/server/layer/body_limit.rs | 1 - volo-http/src/server/utils/multipart.rs | 12 +++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/volo-http/src/server/layer/body_limit.rs b/volo-http/src/server/layer/body_limit.rs index 24c2778e..23381f1e 100644 --- a/volo-http/src/server/layer/body_limit.rs +++ b/volo-http/src/server/layer/body_limit.rs @@ -18,7 +18,6 @@ impl BodyLimitLayer { /// Create a new [`BodyLimitLayer`] with given `body_limit`. /// /// If the Body is larger than the `body_limit`, the request will be rejected. - /// ``` pub fn new(body_limit: usize) -> Self { Self { limit: body_limit } } diff --git a/volo-http/src/server/utils/multipart.rs b/volo-http/src/server/utils/multipart.rs index 47c4091e..f20c9de2 100644 --- a/volo-http/src/server/utils/multipart.rs +++ b/volo-http/src/server/utils/multipart.rs @@ -79,20 +79,22 @@ use crate::{ /// ```rust /// use http::StatusCode; /// use volo_http::{ -/// Router, /// server::{ /// layer::BodyLimitLayer, /// route::post, /// utils::multipart::{Multipart, MultipartRejectionError}, -/// } +/// }, +/// Router, /// }; /// -/// async fn upload_handler(mut multipart: Multipart) -> Result { +/// async fn upload_handler( +/// mut multipart: Multipart, +/// ) -> Result { /// Ok(StatusCode::OK) /// } /// -/// let app: Router<_>= Router::new() -/// .route("/",post(upload_handler)) +/// let app: Router<_> = Router::new() +/// .route("/", post(upload_handler)) /// .layer(BodyLimitLayer::new(1024)); /// ``` #[must_use]