From 7ab08ad55ea0e4a470f3e184b1a49d145998ddbf Mon Sep 17 00:00:00 2001 From: glendc Date: Thu, 4 Apr 2024 11:32:57 +0200 Subject: [PATCH] fix, improve and expand http form example --- examples/http_form.rs | 101 +++++++++++++++--- src/http/service/web/endpoint/extract/body.rs | 62 +++++++++-- src/http/service/web/endpoint/extract/mod.rs | 20 +++- 3 files changed, 156 insertions(+), 27 deletions(-) diff --git a/examples/http_form.rs b/examples/http_form.rs index 793bf7441..e6abce78e 100644 --- a/examples/http_form.rs +++ b/examples/http_form.rs @@ -9,7 +9,7 @@ //! # Run the example //! //! ```sh -//! cargo run --example http_form +//! RUST_LOG=debug cargo run --example http_form //! ``` //! //! # Expected output @@ -17,43 +17,114 @@ //! The server will start and listen on `:8080`. You can use `curl` to check if the server is running: //! //! ```sh -//! curl -X POST -F 'name=John' -F 'age=32' http://127.0.0.1:8080/form +//! curl -X POST http://127.0.0.1:8080/form \ +//! -H "Content-Type: application/x-www-form-urlencoded" \ +//! -d "name=John&age=32" +//! +//! curl -v 'http://127.0.0.1:8080/form?name=John&age=32' //! ``` //! -//! You should see a response with `HTTP/1.1 200 OK` and `John is 32 years old.`. +//! You should see in both cases a response with `HTTP/1.1 200 OK` and `John is 32 years old.`. +//! +//! Alternatively you can +//! +//! ``` +//! open http://127.0.0.1:8080 +//! ``` +//! +//! and fill the form in the browser, you should see a response page after submitting the form, +//! stating your name and age. use rama::http::layer::trace::TraceLayer; -use rama::http::service::web::extract::Form; -use rama::http::service::web::WebService; +use rama::http::matcher::HttpMatcher; +use rama::http::response::Html; +use rama::http::service::web::{extract::Form, WebService}; +use rama::http::{IntoResponse, Response}; use rama::service::ServiceBuilder; use rama::{http::server::HttpServer, rt::Executor}; use serde::{Deserialize, Serialize}; +use std::time::Duration; +use tracing::level_filters::LevelFilter; +use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; #[derive(Serialize, Deserialize, Debug)] struct Payload { name: String, age: i32, + html: Option, } #[tokio::main] async fn main() { - let exec = Executor::default(); - HttpServer::auto(exec) - .listen( - "127.0.0.1:8080", - ServiceBuilder::new() - .layer(TraceLayer::new_for_http()) - .service(WebService::default().post("/form", send_form_data)), + tracing_subscriber::registry() + .with(fmt::layer()) + .with( + EnvFilter::builder() + .with_default_directive(LevelFilter::DEBUG.into()) + .from_env_lossy(), ) + .init(); + + let graceful = rama::graceful::Shutdown::default(); + + graceful.spawn_task_fn(|guard| async move { + let exec = Executor::graceful(guard.clone()); + HttpServer::auto(exec) + .listen_graceful( + guard, + "127.0.0.1:8080", + ServiceBuilder::new() + .layer(TraceLayer::new_for_http()) + .service( + WebService::default() + .get( + "/", + Html( + r##" + +
+
+
+
+

+
+ +
+ + "##, + ), + ) + .on(HttpMatcher::method_get().or_method_post().and_path("/form"), send_form_data), + ), + ) + .await + .expect("failed to run service"); + }); + + graceful + .shutdown_with_limit(Duration::from_secs(30)) .await - .expect("failed to run service"); + .expect("graceful shutdown"); } -async fn send_form_data(Form(payload): Form) -> String { +async fn send_form_data(Form(payload): Form) -> Response { tracing::info!("{:?}", payload.name); let name = payload.name; let age = payload.age; - format!("{} is {} years old.", name, age) + if payload.html.unwrap_or_default() { + Html(format!( + r##" + +

Success

+

Thank you for submitting the form {}, {} years old.

+ + "##, + name, age + )) + .into_response() + } else { + format!("{} is {} years old.", name, age).into_response() + } } diff --git a/src/http/service/web/endpoint/extract/body.rs b/src/http/service/web/endpoint/extract/body.rs index 924525153..b0e388d14 100644 --- a/src/http/service/web/endpoint/extract/body.rs +++ b/src/http/service/web/endpoint/extract/body.rs @@ -1,5 +1,5 @@ use super::FromRequest; -use crate::http::{self, dep::http_body_util::BodyExt, StatusCode}; +use crate::http::{self, dep::http_body_util::BodyExt, Method, StatusCode}; use crate::service::Context; use std::convert::Infallible; use std::ops::{Deref, DerefMut}; @@ -141,15 +141,31 @@ where type Rejection = StatusCode; async fn from_request(_ctx: Context, req: http::Request) -> Result { - let body = req.into_body(); - match body.collect().await { - Ok(c) => { - let b = c.to_bytes(); - let value = - serde_urlencoded::from_bytes(&b).map_err(|_| StatusCode::BAD_REQUEST)?; - Ok(Form(value)) + if req.method() == Method::GET { + let value = match req.uri().query() { + Some(query) => serde_urlencoded::from_bytes(query.as_bytes()), + None => serde_urlencoded::from_bytes(&[]), + } + .map_err(|_| StatusCode::BAD_REQUEST)?; + Ok(Form(value)) + } else { + if !super::has_any_content_type( + req.headers(), + &[&mime::APPLICATION_WWW_FORM_URLENCODED], + ) { + return Err(StatusCode::BAD_REQUEST); + } + + let body = req.into_body(); + match body.collect().await { + Ok(c) => { + let b = c.to_bytes(); + let value = + serde_urlencoded::from_bytes(&b).map_err(|_| StatusCode::BAD_REQUEST)?; + Ok(Form(value)) + } + Err(_) => Err(StatusCode::BAD_REQUEST), } - Err(_) => Err(StatusCode::BAD_REQUEST), } } } @@ -227,7 +243,30 @@ mod test { } #[tokio::test] - async fn test_form() { + async fn test_form_post_form_urlencoded() { + #[derive(Debug, serde::Deserialize)] + struct Input { + name: String, + age: u8, + } + + let service = WebService::default().post("/", |Form(body): Form| async move { + assert_eq!(body.name, "Devan"); + assert_eq!(body.age, 29); + }); + + let req = http::Request::builder() + .uri("/") + .method(http::Method::POST) + .header("content-type", "application/x-www-form-urlencoded") + .body(r#"name=Devan&age=29"#.into()) + .unwrap(); + let resp = service.serve(Context::default(), req).await.unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + } + + #[tokio::test] + async fn test_form_get() { #[derive(Debug, serde::Deserialize)] struct Input { name: String, @@ -240,8 +279,9 @@ mod test { }); let req = http::Request::builder() + .uri("/?name=Devan&age=29") .method(http::Method::GET) - .body(r#"name=Devan&age=29"#.into()) + .body(http::Body::empty()) .unwrap(); let resp = service.serve(Context::default(), req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); diff --git a/src/http/service/web/endpoint/extract/mod.rs b/src/http/service/web/endpoint/extract/mod.rs index 6536fdc50..a08d51b61 100644 --- a/src/http/service/web/endpoint/extract/mod.rs +++ b/src/http/service/web/endpoint/extract/mod.rs @@ -1,6 +1,6 @@ //! Extract utilities to develop endpoint services efortless. -use crate::http::{self, dep::http::request::Parts, IntoResponse}; +use crate::http::{self, dep::http::request::Parts, dep::mime, header, HeaderMap, IntoResponse}; use crate::service::Context; use std::future::Future; @@ -94,3 +94,21 @@ where Self::from_request_parts(&ctx, &parts).await } } + +fn has_any_content_type(headers: &HeaderMap, expected_content_types: &[&mime::Mime]) -> bool { + let content_type = if let Some(content_type) = headers.get(header::CONTENT_TYPE) { + content_type + } else { + return false; + }; + + let content_type = if let Ok(content_type) = content_type.to_str() { + content_type + } else { + return false; + }; + + expected_content_types + .iter() + .any(|ct| content_type.starts_with(ct.as_ref())) +}