--- url: /guide/concepts/catcher.md --- # Catcher When a `Response` returns an error status code and the `Body` within the page is empty, Salvo will attempt to catch this error using a `Catcher` and display a user-friendly error page. You can obtain a system-default `Catcher` by calling `Catcher::default()`, and then add it to the `Service`. ```rust use salvo::catcher::Catcher; Service::new(router).catcher(Catcher::default()); ``` The default `Catcher` supports sending error pages in `XML`, `JSON`, `HTML`, and `Text` formats. You can add custom error-catching handlers to the `Catcher` by attaching `hoop`s to this default `Catcher`. These error-catching handlers are still of type `Handler`. You can add multiple custom error-catching handlers to the `Catcher` via `hoop`s. Custom error handlers can call the `FlowCtrl::skip_next` method after processing an error to skip subsequent error handlers and return early. **main.rs** ```rust file="/codes/custom-error-page/src/main.rs" use salvo::catcher::Catcher; use salvo::prelude::*; // Handler that returns a simple "Hello World" response #[handler] async fn hello() -> &'static str { "Hello World" } // Handler that deliberately returns a 500 Internal Server Error #[handler] async fn error500(res: &mut Response) { res.status_code(StatusCode::INTERNAL_SERVER_ERROR); } #[tokio::main] async fn main() { // Initialize logging system tracing_subscriber::fmt().init(); // Create and start server with custom error handling let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(create_service()).await; } // Create service with custom error handling fn create_service() -> Service { // Set up router with two endpoints: // - / : Returns "Hello World" // - /500 : Triggers a 500 error let router = Router::new() .get(hello) .push(Router::with_path("500").get(error500)); // Add custom error catcher for 404 Not Found errors Service::new(router).catcher(Catcher::default().hoop(handle404)) } // Custom handler for 404 Not Found errors #[handler] async fn handle404(&self, _req: &Request, _depot: &Depot, res: &mut Response, ctrl: &mut FlowCtrl) { // Check if the error is a 404 Not Found if StatusCode::NOT_FOUND == res.status_code.unwrap_or(StatusCode::NOT_FOUND) { // Return custom error page res.render("Custom 404 Error Page"); // Skip remaining error handlers ctrl.skip_rest(); } } ``` **Cargo.toml** ```toml file="/codes/custom-error-page/Cargo.toml" [package] name = "example-custom-error-page" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` --- url: /guide/concepts/depot.md --- # Depot [Depot](https://docs.rs/salvo_core/latest/salvo_core/struct.Depot.html) is used to store temporary data involved in a single request. Middleware can place the temporary data it processes into the Depot for use by subsequent programs. When a server receives a request from a client browser, it creates an instance of `Depot`. This instance is destroyed after all middleware and `Handler` have finished processing the request. For example, we can set `current_user` in a login middleware and then read the current user information in subsequent middleware or `Handler`. ## Quick Understanding `Depot` is used to store and share data during request processing. It implements a type-safe data container that supports two main usage patterns: 1. **Key-Value Storage**: Access values associated with string keys via `insert`/`get` methods. 2. **Type Injection**: Store and retrieve values based on type via `insert_typed`/`get_typed` methods. As shown in the examples, `Depot` is particularly useful for passing data between middleware and handlers. Middleware can set values in `Depot` (such as user information, authentication status), and subsequent handlers can retrieve these values without redundant calculations or queries. The design of `Depot` ensures data consistency and accessibility throughout the entire request processing chain, making it a core tool for building complex web applications. **main.rs** ```rust file="/codes/use-depot/src/main.rs" use salvo::prelude::*; #[handler] async fn set_user(depot: &mut Depot) { depot.insert("user", "client"); } #[handler] async fn hello(depot: &mut Depot) -> String { format!( "Hello {}", depot.get::<&str>("user").copied().unwrap_or_default() ) } #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); let router = Router::new().hoop(set_user).goal(hello); let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } ``` **Cargo.toml** ```toml file="/codes/use-depot/Cargo.toml" [package] name = "example-use-depot" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` ## Setting and Retrieving Data via `insert` and `get` As shown above, you can insert `key` and `value` into `Depot` using `insert`. For values of this type, you can directly retrieve them using `get`. ```rust depot.insert("a", "b"); assert_eq!(depot.get::<&str>("a").copied().unwrap(), "b") ``` If the `key` does not exist, or if the `key` exists but the type does not match, it returns `None`. ## Setting and Retrieving Data via `insert_typed` and `get_typed` Sometimes, there are scenarios where a unique instance exists for a type without needing to relate to a specific `key`. You can use `insert_typed` to insert data and then use `get_typed` to retrieve it. These methods do not require you to provide a `key`. ```rust depot.insert_typed(Config::new()); depot.get_typed::(); ``` # Overview of Depot Struct Methods | Category | Method | Description | | ---------------------------- | ------------------------- | ------------------------------------------------------------------ | | **Creation/Capacity** | `new()` | Creates an empty Depot | | | `with_capacity(capacity)` | Creates a Depot with the specified capacity | | | `capacity()` | Gets the capacity | | | `inner()` | Gets a reference to the internal HashMap | | **Type Injection/Retrieval** | `insert_typed(value)` | Injects a value by type | | | `get_typed()` | Retrieves a reference to the injected value | | | `get_typed_mut()` | Retrieves a mutable reference to the injected value | | | `contains_typed()` | Checks if a specific type is contained | | | `remove_typed()` | Removes and returns the injected value | | **Key-Value Operations** | `insert(key, value)` | Inserts a key-value pair | | | `get(key)` | Retrieves a reference to the value associated with the key | | | `get_mut(key)` | Retrieves a mutable reference to the value associated with the key | | | `contains_key(key)` | Checks if a specific key is contained | | | `remove(key)` | Removes the key and returns the value | | | `remove(key).is_some()` | Deletes the key-value pair | --- url: /guide/concepts/handler.md --- # Handler ## Quick Overview Handler is a core concept in the Salvo framework, which can be simply understood as a request processing unit. It has two main purposes: 1. **As an Endpoint**: An object implementing `Handler` can be placed into the routing system as the final endpoint to process requests. When using the `#[handler]` macro, a function can be directly used as an endpoint; while using the `#[endpoint]` macro not only allows it to serve as an endpoint but also automatically generates OpenAPI documentation (this will be detailed in subsequent documentation). 2. **As Middleware**: The same `Handler` can also be used as middleware to process requests before or after they reach the final endpoint. Salvo's request processing flow can be viewed as a "pipeline": a request first passes through a series of middleware (vertical processing) and then reaches the matching endpoint (horizontal processing). Both middleware and endpoints are implementations of `Handler`, which ensures consistency and flexibility throughout the system. ### Handler Flowchart in Salvo ```mermaid flowchart TD Client[Client] -->|Request| Request[HTTP Request] Request --> M1 subgraph "Handler Implementation" M1[Logging Middleware] -->|Vertical Processing| M2[Auth Middleware] -->|Vertical Processing| M3[Other Business Middleware] M3 --> Router[Route Matching] Router -->|Horizontal Processing| E1["Endpoint 1
#[handler]"] Router -->|Horizontal Processing| E2["Endpoint 2
#[endpoint]"] Router -->|Horizontal Processing| E3["Endpoint 3
Implements Handler Trait"] end E1 --> Response[HTTP Response] E2 --> Response E3 --> Response Response --> Client classDef middleware fill:#afd,stroke:#4a4,stroke-width:2px; classDef endpoint fill:#bbf,stroke:#55a,stroke-width:2px; class M1,M2,M3 middleware; class E1,E2,E3 endpoint; ``` ### Middleware and the Onion Model The essence of the onion model is that by placing `ctrl.call_next()` before and after specific logic, it implements a bidirectional processing flow for requests and responses, allowing each middleware to participate in the complete request-response cycle. ### Complete Middleware Example Structure ```rust async fn example_middleware(req: &mut Request, depot: &mut Depot, resp: &mut Response, ctrl: &mut FlowCtrl) { // Pre-processing (Request Phase) // Place logic to execute when the request enters here. // Call the next handler in the chain. ctrl.call_next(req, depot, resp).await; // Post-processing (Response Phase) // Place logic to execute after the request is processed here. } ``` ```mermaid flowchart TB Client[Client] -->|Request| Request[HTTP Request] subgraph "Onion Model Processing Flow" direction TB subgraph M1["Middleware 1"] M1A["Pre-processing
Logging: Request Start"] --> M1B["Post-processing
Logging: Request End"] end subgraph M2["Middleware 2"] M2A["Pre-processing
Record Request Start Time"] --> M2B["Post-processing
Calculate Processing Time"] end subgraph M3["Middleware 3"] M3A["Pre-processing
Validate User Permissions"] --> M3B["Post-processing
"] end subgraph Core["Core Processing Logic"] Handler["Endpoint Handler
#[handler]"] end Request --> M1A M1A --> M2A M2A --> M3A M3A --> Handler Handler --> M3B M3B --> M2B M2B --> M1B M1B --> Response end Response[HTTP Response] --> Client classDef middleware1 fill:#f9d,stroke:#333,stroke-width:2px; classDef middleware2 fill:#afd,stroke:#333,stroke-width:2px; classDef middleware3 fill:#bbf,stroke:#333,stroke-width:2px; classDef core fill:#fd7,stroke:#333,stroke-width:2px; class M1A,M1B middleware1; class M2A,M2B middleware2; class M3A,M3B middleware3; class Handler core; ``` ## What is a Handler A Handler is the concrete object responsible for processing Request objects. Handler itself is a Trait containing an asynchronous `handle` method: ```rust #[async_trait] pub trait Handler: Send + Sync + 'static { async fn handle(&self, req: &mut Request, depot: &mut Depot, res: &mut Response); } ``` The default signature of the `handle` function includes four parameters, in order: `&mut Request, &mut Depot, &mut Response, &mut FlowCtrl`. Depot is temporary storage that can hold data related to the current request. Depending on how it's used, it can serve as middleware (hoop), which can perform processing before or after the request reaches the formal request-processing `Handler`, such as: login verification, data compression, etc. Middleware is added via the `hoop` function of a `Router`. The added middleware affects the current `Router` and all its descendant `Router`s. A `Handler` can also be used as a `Handler` that participates in route matching and is ultimately executed, known as a `goal`. ## `Handler` as Middleware (hoop) When a `Handler` acts as middleware, it can be added to the following three types of objects that support middleware: - `Service`: Any request will pass through the middleware in the `Service`. - `Router`: Only when route matching succeeds will the request pass through the middleware defined in the `Service` and all middleware collected along the matching path. - `Catcher`: When an error occurs and no custom error information has been written, the request will pass through the middleware in the `Catcher`. - `Handler`: The `Handler` itself supports adding middleware wrappers to execute some pre- or post-logic. ## Using the `#[handler]` Macro The `#[handler]` macro can greatly simplify code writing and improve code flexibility. It can be applied to a function to make it implement `Handler`: ```rust #[handler] async fn hello() -> &'static str { "hello world!" } ``` This is equivalent to: ```rust struct hello; #[async_trait] impl Handler for hello { async fn handle(&self, _req: &mut Request, _depot: &mut Depot, res: &mut Response, _ctrl: &mut FlowCtrl) { res.render(Text::Plain("hello world!")); } } ``` As you can see, with `#[handler]`, the code becomes much simpler: - No need to manually add `#[async_trait]`. - Unnecessary parameters in the function are omitted, and the required parameters can be arranged in any order. - For objects implementing the `Writer` or `Scribe` abstractions, they can be directly returned as the function's return value. Here, `&'static str` implements `Scribe`, so it can be directly returned. `#[handler]` can not only be applied to functions but also to the `impl` block of a `struct` to make the `struct` implement `Handler`. In this case, the `handle` function within the `impl` block is recognized as the concrete implementation of the `handle` method in `Handler`: ```rust struct Hello; #[handler] impl Hello { async fn handle(&self, res: &mut Response) { res.render(Text::Plain("hello world!")); } } ``` ## Handling Errors In Salvo, a `Handler` can return a `Result`, provided that both the `Ok` and `Err` types within the `Result` implement the `Writer` trait. Considering the widespread use of anyhow, when the `anyhow` feature is enabled, `anyhow::Error` will implement the `Writer` trait. `anyhow::Error` will be mapped to `InternalServerError`. ```rust #[cfg(feature = "anyhow")] #[async_trait] impl Writer for ::anyhow::Error { async fn write(mut self, _req: &mut Request, _depot: &mut Depot, res: &mut Response) { res.render(StatusError::internal_server_error()); } } ``` For custom error types, you can output different error pages as needed. ```rust use salvo::anyhow; use salvo::prelude::*; struct CustomError; #[async_trait] impl Writer for CustomError { async fn write(mut self, _req: &mut Request, _depot: &mut Depot, res: &mut Response) { res.status_code(StatusCode::INTERNAL_SERVER_ERROR); res.render("custom error"); } } #[handler] async fn handle_anyhow() -> Result<(), anyhow::Error> { Err(anyhow::anyhow!("anyhow error")) } #[handler] async fn handle_custom() -> Result<(), CustomError> { Err(CustomError) } #[tokio::main] async fn main() { let router = Router::new() .push(Router::new().path("anyhow").get(handle_anyhow)) .push(Router::new().path("custom").get(handle_custom)); let acceptor = TcpListener::new("127.0.0.1:8698").bind().await; Server::new(acceptor).serve(router).await; } ``` ## Directly Implementing the Handler Trait ```rust use salvo_core::prelude::*; use crate::salvo_core::http::Body; pub struct MaxSizeHandler(u64); #[async_trait] impl Handler for MaxSizeHandler { async fn handle(&self, req: &mut Request, depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl) { if let Some(upper) = req.body().and_then(|body| body.size_hint().upper()) { if upper > self.0 { res.render(StatusError::payload_too_large()); ctrl.skip_rest(); } else { ctrl.call_next(req, depot, res).await; } } } } ``` --- url: /guide/concepts/index.md --- # Concepts ## Concepts ### [Router](/guide/concepts/router.md) - [What is Routing](/guide/concepts/router.md#what-is-routing) - [Flat Definition](/guide/concepts/router.md#flat-definition) - [Tree-like Definition](/guide/concepts/router.md#tree-like-definition) - [Retrieving Parameters from Routes](/guide/concepts/router.md#retrieving-parameters-from-routes) - [Adding Middleware](/guide/concepts/router.md#adding-middleware) - [Filters](/guide/concepts/router.md#filters) - [Custom Wisp](/guide/concepts/router.md#custom-wisp) - [Coming from a Controller-based Web Framework, How to Understand Router?](/guide/concepts/router.md#coming-from-a-controller-based-web-framework-how-to-understand-router) ### [Request](/guide/concepts/request.md) - [Quick Overview](/guide/concepts/request.md#quick-overview) - [Getting Query Parameters](/guide/concepts/request.md#getting-query-parameters) - [Getting Form Data](/guide/concepts/request.md#getting-form-data) - [Getting JSON Deserialized Data](/guide/concepts/request.md#getting-json-deserialized-data) - [Extracting Request Data](/guide/concepts/request.md#extracting-request-data) - [Parsing Principle](/guide/concepts/request.md#parsing-principle) - [Partial API Overview. For the latest and most detailed information, please refer to the crates API documentation.](/guide/concepts/request.md#partial-api-overview-for-the-latest-and-most-detailed-information-please-refer-to-the-crates-api-documentation) ### [Response](/guide/concepts/response.md) - [Writing Content](/guide/concepts/response.md#writing-content) - [Writing HTTP Errors](/guide/concepts/response.md#writing-http-errors) - [Redirecting to Another URL](/guide/concepts/response.md#redirecting-to-another-url) - [ResBody](/guide/concepts/response.md#resbody) ### [Depot](/guide/concepts/depot.md) - [Quick Understanding](/guide/concepts/depot.md#quick-understanding) - [Setting and Retrieving Data via `insert` and `get`](/guide/concepts/depot.md#setting-and-retrieving-data-via-insert-and-get) - [Setting and Retrieving Data via `insert_typed` and `get_typed`](/guide/concepts/depot.md#setting-and-retrieving-data-via-insert_typed-and-get_typed) ### [Handler](/guide/concepts/handler.md) - [Quick Overview](/guide/concepts/handler.md#quick-overview) - [What is a Handler](/guide/concepts/handler.md#what-is-a-handler) - [`Handler` as Middleware (hoop)](/guide/concepts/handler.md#handler-as-middleware-hoop) - [Using the `#[handler]` Macro](/guide/concepts/handler.md#using-the-handler-macro) - [Handling Errors](/guide/concepts/handler.md#handling-errors) - [Directly Implementing the Handler Trait](/guide/concepts/handler.md#directly-implementing-the-handler-trait) ### [Catcher](/guide/concepts/catcher.md) ### [Writer](/guide/concepts/writer.md) --- url: /guide/concepts/request.md --- # Request In Salvo, user request data can be obtained through [`Request`](https://docs.rs/salvo_core/latest/salvo_core/http/request/struct.Request.html): ## Quick Overview Request is a struct representing an HTTP request, providing comprehensive request handling capabilities: - Operates on basic attributes (URI, method, version) - Handles request headers and Cookies - Parses various parameters (path, query, form) - Supports request body processing and file uploads - Offers multiple data parsing methods (JSON, form, etc.) - Implements unified type-safe data extraction via the extract method ```rust #[handler] async fn hello(req: &mut Request) -> String { req.params().get("id").cloned().unwrap_or_default() } ``` ## Getting Query Parameters Query parameters can be obtained via `get_query`: ```rust req.query::("id"); ``` ## Getting Form Data Form data can be obtained via `get_form`. This function is asynchronous: ```rust req.form::("id").await; ``` ## Getting JSON Deserialized Data ```rust req.parse_json::().await; ``` ## Extracting Request Data `Request` provides multiple methods to parse this data into strongly-typed structures. - `parse_params`: Parses the request's router params into a specific data type. - `parse_queries`: Parses the request's URL queries into a specific data type. - `parse_headers`: Parses the request's HTTP headers into a specific data type. - `parse_json`: Parses the data in the HTTP body part of the request as JSON format into a specific type. - `parse_form`: Parses the data in the HTTP body part of the request as a Form into a specific type. - `parse_body`: Parses the data in the HTTP body part into a specific type based on the request's `content-type`. - `extract`: Can merge different data sources to parse out a specific type. ## Parsing Principle Here, a custom `serde::Deserializer` is used to extract data from structures like `HashMap` and `HashMap>` into specific data types. For example: `URL queries` are actually extracted as a [MultiMap](https://docs.rs/multimap/latest/multimap/struct.MultiMap.html) type. `MultiMap` can be thought of as a data structure similar to `HashMap>`. If the requested URL is `http://localhost/users?id=123&id=234`, and our target type is: ```rust #[derive(Deserialize)] struct User { id: i64 } ``` Then the first `id=123` will be parsed, and `id=234` will be discarded: ```rust let user: User = req.parse_queries().unwrap(); assert_eq!(user.id, 123); ``` If our provided type is: ```rust #[derive(Deserialize)] struct Users { ids: Vec } ``` Then both `id=123&id=234` will be parsed: ```rust let users: Users = req.parse_queries().unwrap(); assert_eq!(user.ids, vec![123, 234]); ``` ### Built-in Extractors The framework includes built-in request parameter extractors. These extractors can significantly simplify code for handling HTTP requests. :::tip To use them, you need to add the `"oapi" feature` in your `Cargo.toml` ```rust salvo = { version = "0.95.0", features = ["oapi"] } ``` ::: Then you can import the extractors: ```rust use salvo::{oapi::extract::JsonBody, prelude::*}; ``` #### JsonBody Used to extract JSON data from the request body and deserialize it into a specified type. ```rust #[handler] async fn create_user(json: JsonBody) -> String { let user = json.into_inner(); format!("Created user with ID {}", user.id) } ``` #### FormBody Extracts form data from the request body and deserializes it into a specified type. ```rust #[handler] async fn update_user(form: FormBody) -> String { let user = form.into_inner(); format!("Updated user with ID {}", user.id) } ``` #### CookieParam Extracts a specific value from the request's Cookies. ```rust // The second parameter: if true, into_inner() will panic if the value doesn't exist. // If false, into_inner() returns Option. #[handler] fn get_user_from_cookie(user_id: CookieParam) -> String { format!("User ID from Cookie: {}", user_id.into_inner()) } ``` #### HeaderParam Extracts a specific value from the request headers. ```rust #[handler] fn get_user_from_header(user_id: HeaderParam) -> String { format!("User ID from Header: {}", user_id.into_inner()) } ``` #### PathParam Extracts parameters from the URL path. ```rust #[handler] fn get_user(id: PathParam) -> String { format!("User ID from Path: {}", id.into_inner()) } ``` #### QueryParam Extracts parameters from the URL query string. ```rust #[handler] fn search_user(id: QueryParam) -> String { format!("Searching for user with ID: {}", id.into_inner()) } ``` ### Extracting from Depot You can extract data from `Depot` that was injected by middleware. This is useful for accessing authenticated user information or other request-scoped data. ```rust /// Middleware that injects user data into depot #[handler] async fn inject_user(depot: &mut Depot) { depot.insert("user_id", 123i64); depot.insert("username", "alice".to_string()); depot.insert("is_admin", true); } /// Extract user context from depot #[derive(Serialize, Deserialize, Extractible, Debug)] #[salvo(extract(default_source(from = "depot")))] struct UserContext { user_id: i64, username: String, is_admin: bool, } #[handler] async fn protected_handler(user: UserContext) -> String { format!("Hello {}, your ID is {}", user.username, user.user_id) } // Router setup with middleware let router = Router::new() .hoop(inject_user) .push(Router::with_path("protected").get(protected_handler)); ``` Depot extraction supports the following types: - `String` and `&'static str` - Signed integers: `i8`, `i16`, `i32`, `i64`, `i128`, `isize` - Unsigned integers: `u8`, `u16`, `u32`, `u64`, `u128`, `usize` - Floating point: `f32`, `f64` - `bool` You can also mix depot with other sources: ```rust #[derive(Serialize, Deserialize, Extractible, Debug)] struct RequestData { #[salvo(extract(source(from = "depot")))] user_id: i64, #[salvo(extract(source(from = "query")))] page: i64, #[salvo(extract(source(from = "body")))] content: String, } ``` ### Advanced Usage You can merge multiple data sources to parse a specific type. First, define a custom type, for example: ```rust #[derive(Serialize, Deserialize, Extractible, Debug)] /// By default, gets field values from the body. #[salvo(extract(default_source(from = "body")))] struct GoodMan<'a> { /// The id is obtained from the request path parameters and automatically parsed as i64. #[salvo(extract(source(from = "param")))] id: i64, /// Reference types can be used to avoid memory copying. username: &'a str, first_name: String, last_name: String, } ``` Then in a `Handler`, you can get the data like this: ```rust #[handler] async fn edit(req: &mut Request, depot: &mut Depot) { let good_man: GoodMan<'_> = req.extract(depot).await.unwrap(); } ``` You can even pass the type directly as a function parameter, like this: ```rust #[handler] async fn edit<'a>(good_man: GoodMan<'a>) { res.render(Json(good_man)); } ``` Data type definitions offer considerable flexibility, even allowing parsing into nested structures as needed: ```rust #[derive(Serialize, Deserialize, Extractible, Debug)] #[salvo(extract(default_source(from = "body")))] struct GoodMan<'a> { #[salvo(extract(source(from = "param")))] id: i64, #[salvo(extract(source(from = "query")))] username: &'a str, first_name: String, last_name: String, lovers: Vec, /// This nested field is parsed entirely from the Request again. #[salvo(extract(flatten))] nested: Nested<'a>, } #[derive(Serialize, Deserialize, Extractible, Debug)] #[salvo(extract(default_source(from = "body")))] struct Nested<'a> { #[salvo(extract(source(from = "param")))] id: i64, #[salvo(extract(source(from = "query")))] username: &'a str, first_name: String, last_name: String, #[salvo(extract(rename = "lovers"))] #[serde(default)] pets: Vec, } ``` For a concrete example, see: [extract-nested](https://github.com/salvo-rs/salvo/blob/main/examples/extract-nested/src/main.rs). ### `#[salvo(extract(flatten))]` VS `#[serde(flatten)]` To flatten an extractible sub-struct, use `#[salvo(extract(flatten))]`. Its fields are extracted from the same request sources as the parent, so they may even share field names with the parent. `#[serde(flatten)]` is **not supported** on `#[derive(Extractible)]` fields: it drives serde's own flattening, which conflicts with Salvo's extraction and used to surface as confusing "missing field" errors at request time. Salvo now rejects it at compile time with a message pointing you to `#[salvo(extract(flatten))]`. ### `#[salvo(extract(source(parse)))]` You can also add a `parse` parameter to `source` to specify a particular parsing method. If this parameter is not specified, parsing is determined based on the `Request` information. For a `Form` body, it's parsed as `MultiMap`; for a JSON payload, it's parsed as JSON. Generally, you don't need to specify this parameter. In rare cases, specifying it can enable special functionality. ```rust #[tokio::test] async fn test_de_request_with_form_json_str() { #[derive(Deserialize, Eq, PartialEq, Debug)] struct User<'a> { name: &'a str, age: usize, } #[derive(Deserialize, Extractible, Eq, PartialEq, Debug)] #[salvo(extract(default_source(from = "body", parse = "json")))] struct RequestData<'a> { #[salvo(extract(source(from = "param")))] p2: &'a str, user: User<'a>, } let mut req = TestClient::get("http://127.0.0.1:8698/test/1234/param2v") .raw_form(r#"user={"name": "chris", "age": 20}"#) .build(); req.params.insert("p2".into(), "921".into()); let mut depot = Depot::new(); let data: RequestData = req.extract(&mut depot).await.unwrap(); assert_eq!( data, RequestData { p2: "921", user: User { name: "chris", age: 20 } } ); } ``` For instance, here the actual request sends a Form, but the value of a certain field is a piece of JSON text. By specifying `parse`, this string can be parsed in JSON format. ## Partial API Overview. For the latest and most detailed information, please refer to the crates API documentation. # Request Struct Method Overview | Category | Method | Description | | ---------------------- | -------------------------------------- | ------------------------------------------- | | **Request Info** | `uri()/uri_mut()/set_uri()` | URI operations | | | `method()/method_mut()` | HTTP method operations | | | `version()/version_mut()` | HTTP version operations | | | `scheme()/scheme_mut()` | Protocol scheme operations | | | `remote_addr()/local_addr()` | Address information | | **Request Headers** | `headers()/headers_mut()` | Get all request headers | | | `header()/try_header()` | Get and parse a specific header | | | `add_header()` | Add a request header | | | `content_type()/accept()` | Get content type/accept type | | **Parameter Handling** | `params()/param()` | Path parameter operations | | | `queries()/query()` | Query parameter operations | | | `form()/form_or_query()` | Form data operations | | **Request Body** | `body()/body_mut()` | Get the request body | | | `replace_body()/take_body()` | Modify/extract the request body | | | `payload()/payload_with_max_size()` | Get raw data | | **File Handling** | `file()/files()/all_files()` | Get uploaded files | | | `first_file()` | Get the first file | | **Data Parsing** | `extract()` | Unified data extraction | | | `parse_json()/parse_form()` | Parse JSON/form | | | `parse_body()` | Intelligently parse the request body | | | `parse_params()/parse_queries()` | Parse parameters/queries | | **Special Features** | `cookies()/cookie()` | Cookie operations (requires cookie feature) | | | `extensions()/extensions_mut()` | Extension data storage | | | `set_secure_max_size()` | Set secure size limit | --- url: /guide/concepts/response.md --- # Response In a `Handler`, the [`Response`](https://docs.rs/salvo_core/latest/salvo_core/http/response/struct.Response.html) is passed as a parameter: - The `Response` struct encapsulates all components of an HTTP response, providing a comprehensive API for constructing and manipulating HTTP responses. - It supports a fluent, chainable style (e.g., `res.status_code(200).body("Hello")`), facilitating the smooth construction of responses. - Core functionalities include: - Setting status codes and headers - Manipulating the response body (supporting strings, bytes, files, and streaming data) - Managing Cookies - Multiple content rendering methods - This struct employs a mutable reference pattern, returning a reference to itself via `&mut self`, allowing handlers to conveniently build and customize HTTP responses to meet various web service requirements. ```rust #[handler] async fn hello_world(res: &mut Response) { res.render("Hello world!"); } ``` After the server receives a client request, any matched `Handler` or middleware can write data into the `Response`. In certain scenarios, such as when a middleware wishes to prevent the execution of subsequent middleware and `Handler`s, you can use `FlowCtrl`: ```rust #[handler] async fn hello_world(res: &mut Response, ctrl: &mut FlowCtrl) { ctrl.skip_rest(); res.render("Hello world!"); } ``` ## Writing Content Writing data to a `Response` is straightforward: - Writing plain text data ```rust res.render("Hello world!"); ``` - Writing JSON serialized data ```rust use serde::Serialize; use salvo::prelude::Json; #[derive(Serialize, Debug)] struct User { name: String, } let user = User{name: "jobs".to_string()}; res.render(Json(user)); ``` :::warning `Json` renders one complete JSON document. In Salvo 0.94 and later, rendering `Json` replaces any body bytes already buffered on the `Response` instead of appending to them. To emit multiple JSON records, serialize each record yourself and append it with `write_body`, for example when producing NDJSON. ::: - Writing HTML ```rust res.render(Text::Html("hello")); ``` ## Writing HTTP Errors - Using `render` allows writing detailed error information to the `Response`. ```rust use salvo::http::errors::*; res.render(StatusError::internal_server_error().brief("error when serialize object to json")) ``` - If custom error information is not needed, you can directly call `set_http_code`. ```rust use salvo::http::StatusCode; res.status_code(StatusCode::BAD_REQUEST); ``` ## Redirecting to Another URL - Using the `render` method, you can write a redirect response to the `Response`, navigating to a new URL. When you call `Redirect::found`, it sets the HTTP status code to 302 (Found), indicating a temporary redirect. ```rust use salvo::prelude::*; #[handler] async fn redirect(res: &mut Response) { res.render(Redirect::found("https://salvo.rs/")); } ``` ## ResBody The Body type returned by Response is `ResBody`, which is an enum. In case of an error, it is set to `ResBody::Error`, containing error information for deferred error handling. Notably, `StatusError` does not implement `Writer`; this design allows you to customize its display in the `Catcher`. # Overview of Response Struct Methods | Category | Method | Description | | -------------------- | ------------------------------ | ------------------------------------------------- | | **Creation** | `new()` / `with_cookies()` | Create a new response | | **Status Code** | `status_code()` | Set the status code | | **Headers** | `headers()/headers_mut()` | Get headers | | | `set_headers()/add_header()` | Set/Add headers | | | `content_type()` | Get content type | | **HTTP Version** | `version()/version_mut()` | Get/Modify HTTP version | | **Response Body** | `body()/body_mut()` | Set/Get the response body | | | `replace_body()/take_body()` | Replace/Extract the response body | | | `write_body()` | Write data to the response body | | | `stream()/channel()` | Stream response / Create a sending channel | | **Cookie Handling** | `cookies()/cookie()` | Get cookies / Get a specific cookie | | | `add_cookie()/remove_cookie()` | Add/Remove a cookie | | **Content Response** | `render()` | Render content | | | `render_with_status()` | Set status code and render content | | | `send_file()` | Send a file | | **Status Query** | `is_stamped()` | Check if the response is ready to be written back | --- url: /guide/concepts/router.md --- # Router ## What is Routing [`Router`](https://docs.rs/salvo_core/latest/salvo_core/routing/struct.Router.html) defines which middleware and `Handler` will process an HTTP request. This is the most fundamental and core functionality in Salvo. Internally, a `Router` is essentially composed of a series of filters. When a request arrives, the router tests itself and its descendants in order, from top to bottom, to see if they can match the request. If a match is successful, the middleware on the entire chain formed by the router and its descendant routers are executed sequentially. If during processing, the `Response` status is set to an error (4XX, 5XX) or a redirect (3XX), subsequent middleware and `Handler` will be skipped. You can also manually call `ctrl.skip_rest()` to skip the remaining middleware and `Handler`. During the matching process, there exists a URL path information object, which can be considered as an object that must be completely consumed by filters during matching. If all filters in a certain Router match successfully, and this URL path information has been fully consumed, it is considered a "successful match". For example: ```rust Router::with_path("articles").get(list_articles).post(create_article); ``` Is actually equivalent to: ```rust Router::new() // PathFilter can filter request paths. It only matches successfully if the request path contains the "articles" segment. // Otherwise, the match fails. For example: /articles/123 matches successfully, while /articles_list/123 // contains "articles" but fails to match because of the trailing "_list". .filter(PathFilter::new("articles")) // If the root matches successfully and the request method is GET, the inner child router can match successfully, // and the request is handled by list_articles. .push(Router::new().filter(filters::get()).handle(list_articles)) // If the root matches successfully and the request method is POST, the inner child router can match successfully, // and the request is handled by create_article. .push(Router::new().filter(filters::post()).handle(create_article)); ``` If accessing `GET /articles/`, it is considered a successful match, and `list_articles` is executed. However, if accessing `GET /articles/123`, the route match fails and returns a 404 error because `Router::with_path("articles")` only consumes the `/articles` part of the URL path information, leaving the `/123` part unconsumed, thus the match is considered failed. To achieve a successful match, the route can be changed to: ```rust Router::with_path("articles/{**}").get(list_articles).post(create_article); ``` Here, `{**}` matches any remaining path, so it can match `GET /articles/123` and execute `list_articles`. ## Flat Definition We can define routes in a flat style: ```rust Router::with_path("writers").get(list_writers).post(create_writer); Router::with_path("writers/{id}").get(show_writer).patch(edit_writer).delete(delete_writer); Router::with_path("writers/{id}/articles").get(list_writer_articles); ``` ## Tree-like Definition We can also define routes in a tree-like structure, which is the recommended approach: ```rust Router::with_path("writers") .get(list_writers) .post(create_writer) .push( Router::with_path("{id}") .get(show_writer) .patch(edit_writer) .delete(delete_writer) .push(Router::with_path("articles").get(list_writer_articles)), ); ``` This form of definition makes Router definitions hierarchical, clear, and simple for complex projects. Many methods in `Router` return `Self` after being called, facilitating chained code writing. Sometimes, you need to decide how to route based on certain conditions. The routing system also provides the `then` function, which is easy to use: ```rust Router::new() .push( Router::with_path("articles") .get(list_articles) .push(Router::with_path("{id}").get(show_article)) .then(|router|{ if admin_mode() { router.post(create_article).push( Router::with_path("{id}").patch(update_article).delete(delete_writer) ) } else { router } }), ); ``` This example means that routes for creating, editing, and deleting articles are only added when the server is in `admin_mode`. ## Retrieving Parameters from Routes In the code above, `{id}` defines a parameter. We can retrieve its value through the `Request` instance: ```rust #[handler] async fn show_writer(req: &mut Request) { let id = req.param::("id").unwrap(); } ``` `{id}` matches a segment in the path. Normally, an article's `id` is just a number. In this case, we can use a regular expression to restrict the matching rule for `id`, like `r"{id|\d+}"`. For numeric types, there's an even simpler method using ``. Specific notations are: - `{id:num}` matches any number of digit characters. - `{id:num[10]}` matches exactly a specific number of digit characters; here, 10 means it matches exactly 10 digits. - `{id:num(..10)}` matches 1 to 9 digit characters. - `{id:num(3..10)}` matches 3 to 9 digit characters. - `{id:num(..=10)}` matches 1 to 10 digit characters. - `{id:num(3..=10)}` matches 3 to 10 digit characters. - `{id:num(10..)}` matches at least 10 digit characters. You can also match all remaining path segments using `{**}`, `{*+}`, or `{*?}`. For better code readability, you can add appropriate names to make the path semantics clearer, e.g., `{**file_path}`. - `{**}`: Represents a wildcard match where the matched part can be an empty string. For example, the path `/files/{**rest_path}` matches `/files`, `/files/abc.txt`, `/files/dir/abc.txt`. - `{*+}`: Represents a wildcard match where the matched part must exist and cannot be an empty string. For example, the path `/files/{*+rest_path}` does NOT match `/files` but matches `/files/abc.txt`, `/files/dir/abc.txt`. - `{*?}`: Represents a wildcard match where the matched part can be an empty string but can only contain one path segment. For example, the path `/files/{*?rest_path}` does NOT match `/files/dir/abc.txt` but matches `/files`, `/files/abc.txt`. Multiple expressions can be combined to match the same path segment, e.g., `/articles/article_{id:num}/`, `/images/{name}.{ext}`. ## Adding Middleware Middleware can be added via the `hoop` function on a router: ```rust Router::new() .hoop(check_authed) .path("writers") .get(list_writers) .post(create_writer) .push( Router::with_path("{id}") .get(show_writer) .patch(edit_writer) .delete(delete_writer) .push(Router::with_path("articles").get(list_writer_articles)), ); ``` In this example, the root router uses `check_authed` to check if the current user is logged in. All descendant routers are affected by this middleware. If users are only browsing `writer` information and articles, we might prefer they can browse without logging in. We can define the routes like this: ```rust Router::new() .push( Router::new() .hoop(check_authed) .path("writers") .post(create_writer) .push(Router::with_path("{id}").patch(edit_writer).delete(delete_writer)), ) .push( Router::with_path("writers").get(list_writers).push( Router::with_path("{id}") .get(show_writer) .push(Router::with_path("articles").get(list_writer_articles)), ), ); ``` Even though two routers have the same path definition `path("articles")`, they can still be added to the same parent router. ## Filters `Router` internally uses filters to determine if a route matches. Filters support basic logical operations using `or` or `and`. A router can contain multiple filters; when all filters match successfully, the route matches successfully. Website path information is a tree structure, but this tree structure is not equivalent to the tree structure organizing routers. A website path may correspond to multiple router nodes. For example, some content under the `articles/` path may require login to view, while others do not. We can organize subpaths requiring login verification under a router containing login verification middleware, and those not requiring verification under another router without it: ```rust Router::new() .push( Router::with_path("articles") .get(list_articles) .push(Router::new().path("{id}").get(show_article)), ) .push( Router::with_path("articles") .hoop(auth_check) .post(list_articles) .push(Router::new().path("{id}").patch(edit_article).delete(delete_article)), ); ``` Routers use filters to filter requests and send them to the corresponding middleware and `Handler` for processing. `path` and `method` are two of the most commonly used filters. `path` is used to match path information; `method` is used to match the request's Method, e.g., GET, POST, PATCH, etc. We can connect router filters using `and`, `or`: ```rust Router::with_filter(filters::path("hello").and(filters::get())); ``` ### Path Filter Filters based on request paths are the most frequently used. Path filters can define parameters, for example: ```rust Router::with_path("articles/{id}").get(show_article); Router::with_path("files/{**rest_path}").get(serve_file) ``` In a `Handler`, they can be retrieved via the `Request` object's `get_param` function: ```rust #[handler] pub async fn show_article(req: &mut Request) { let article_id = req.param::("id"); } #[handler] pub async fn serve_file(req: &mut Request) { let rest_path = req.param::("rest_path"); } ``` ### Method Filter Filters requests based on the HTTP request's `Method`, for example: ```rust Router::new().get(show_article).patch(update_article).delete(delete_article); ``` Here, `get`, `patch`, `delete` are all Method filters. This is actually equivalent to: ```rust use salvo::routing::filter; let mut root_router = Router::new(); let show_router = Router::with_filter(filters::get()).handle(show_article); let update_router = Router::with_filter(filters::patch()).handle(update_article); let delete_router = Router::with_filter(filters::get()).handle(delete_article); Router::new().push(show_router).push(update_router).push(delete_router); ``` ## Custom Wisp For certain frequently occurring matching expressions, we can assign a short name via `PathFilter::register_wisp_regex` or `PathFilter::register_wisp_builder`. For instance, GUID format often appears in paths. The normal way is to write it like this every time matching is needed: ```rust Router::with_path("/articles/"); Router::with_path("/users/"); ``` Writing this complex regex every time is error-prone and makes code less readable. You can do this instead: ```rust use salvo::routing::filter::PathFilter; #[tokio::main] async fn main() { let guid = regex::Regex::new("[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}").unwrap(); PathFilter::register_wisp_regex("guid", guid); Router::new() .push(Router::with_path("/articles/{id:guid}").get(show_article)) .push(Router::with_path("/users/{id:guid}").get(show_user)); } ``` You only need to register it once. Afterwards, you can directly use simple notation like `{id:guid}` to match GUIDs, simplifying code writing. ## Coming from a Controller-based Web Framework, How to Understand Router? The main differences between routing-designed web frameworks (like Salvo) and traditional MVC or Controller-designed frameworks are: - **Flexibility**: Routing design allows for more flexible definition of request processing flows, enabling precise control over the logic for each path. For example, in Salvo you can directly define handler functions for specific paths: ```rust Router::with_path("articles").get(list_articles).post(create_article); ``` In Controller design, you typically need to define a controller class first, then define multiple methods within the class to handle different requests: ```java @Controller public class ArticleController { @GetMapping("/articles") public List
listArticles() { /* ... */ } @PostMapping("/articles") public Article createArticle(@RequestBody Article article) { /* ... */ } } ``` - **Middleware Integration**: Routing frameworks usually provide a more concise way to integrate middleware, allowing middleware to be applied to specific routes. Salvo's middleware can be precisely applied to particular routes: ```rust Router::new() .push( Router::with_path("admin/articles") .hoop(admin_auth_middleware) // Apply auth middleware only to admin routes .get(list_all_articles) .post(create_article), ) .push( Router::with_path("articles") // Public routes don't need auth .get(list_public_articles), ); ``` - **Code Organization**: Routing design tends to organize code based on functionality or API endpoints, rather than the Model-View-Controller layering of MVC. Routing design encourages organizing code according to API endpoint functionality: ```rust // user_routes.rs - User-related routes and handling logic pub fn user_routes() -> Router { Router::with_path("users") .get(list_users) .post(create_user) .push(Router::with_path("{id}").get(get_user).delete(delete_user)) } // article_routes.rs - Article-related routes and handling logic pub fn article_routes() -> Router { Router::with_path("articles") .get(list_articles) .post(create_article) } // Combine routes in the main application let router = Router::new() .push(user_routes()) .push(article_routes()); ``` - **Lightweight**: Routing design is typically more lightweight, reducing framework-enforced concepts and constraints. You can introduce only the components you need without following a strict framework structure. Routing design makes API development more intuitive, especially suitable for building modern microservices and RESTful APIs. In frameworks like Salvo, routing is a core concept that directly reflects the structure and behavior of the API, making the code easier to understand and maintain. In contrast, traditional Controller design often requires more configuration and conventions to achieve the same functionality. # Router Struct Method Overview | Category | Method | Description | | ----------------------- | ----------------------------------- | --------------------------------------------------- | | **Creation/Access** | `new()` | Creates a new router | | | `routers()/routers_mut()` | Gets a reference/mutable reference to child routers | | | `hoops()/hoops_mut()` | Gets a reference/mutable reference to middleware | | | `filters()/filters_mut()` | Gets a reference/mutable reference to filters | | **Router Organization** | `unshift()` | Inserts a child router at the beginning | | | `insert()` | Inserts a child router at a specified position | | | `push()` | Adds a child router | | | `append()` | Adds multiple child routers | | | `then()` | Customizes router chain configuration | | **Middleware** | `with_hoop()/hoop()` | Creates/adds middleware | | | `with_hoop_when()/hoop_when()` | Creates/adds conditional middleware | | **Path Filtering** | `with_path()/path()` | Creates/adds a path filter | | | `with_filter()/filter()` | Creates/adds a filter | | | `with_filter_fn()/filter_fn()` | Creates/adds a function filter | | **Network Filtering** | `scheme()` | Adds a protocol filter | | | `host()/with_host()` | Adds/creates a host filter | | | `port()/with_port()` | Adds/creates a port filter | | **HTTP Methods** | `get()/post()/put()` | Creates a route for the corresponding HTTP method | | | `delete()/patch()/head()/options()` | Creates a route for the corresponding HTTP method | | **Handler** | `goal()` | Sets the route handler | | **Match Detection** | `detect()` | Detects if the router matches the request | --- url: /guide/concepts/writer.md --- # Writer `Writer` is used to write content into `Response`: ```rust #[async_trait] pub trait Writer { async fn write(mut self, req: &mut Request, depot: &mut Depot, res: &mut Response); } ``` Compared to Handler: ```rust #[async_trait] pub trait Handler: Send + Sync + 'static { async fn handle(&self, req: &mut Request, depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl); } ``` The main differences between them are: - Different purposes: `Writer` represents writing specific content into `Response`, implemented by concrete content such as strings, error messages, etc. In contrast, `Handler` is used to process the entire request. - `Writer` is created within a `Handler` and consumes itself when the `write` function is called, making it a one-time call. On the other hand, `Handler` is shared across all requests. - `Writer` can be returned as the content in the `Result` of a `Handler`. - `Writer` does not include a `FlowCtrl` parameter, so it cannot control the execution flow of the entire request. `Scribe` implements `Writer` but offers fewer capabilities compared to `Writer`: ```rust pub trait Scribe { fn render(self, res: &mut Response); } ``` The rendering function of `Scribe` only writes data into `Response` and cannot retrieve information from `Request` or `Depot` during this process. --- url: /guide/ecology.md --- # Ecosystem Salvo boasts a rich ecosystem, including community-maintained modules, projects built on Salvo, and a wealth of tutorial resources. These resources can help you better utilize the Salvo framework to build high-performance web applications. Additionally, this subdirectory shares some libraries that work well with Salvo. The Rust language is rapidly evolving, and its ecosystem is characterized by numerous small, focused libraries. We hope these commonly used libraries can help you quickly get started with the Rust ecosystem, becoming like a pocketful of handy tools from Doraemon. ## Community Projects Feel free to submit a PR to add your project 😄 - [Socketioxide](https://github.com/Totodore/socketioxide): A Rust implementation of a socket.io server, integrated with the Tower ecosystem and Tokio stack. - [salvo-websocket](https://gitee.com/hubert22/salvo-websocket): WebSocket utilities for the Salvo framework. - [SalvoRsTool](https://github.com/mdddj/SalvoRsTool): A Salvo Idea RustRover plugin for quickly generating DTO, router, and service template code. - [protect-endpoints](https://github.com/DDtKey/protect-endpoints): A collection of components to protect your endpoints. - [salvo-captcha](https://git.4rs.nl/awiteb/salvo-captcha): A captcha middleware for the Salvo framework. - [salvo-casbin](https://github.com/casbin-rs/salvo-casbin): Casbin access control authentication for the Salvo framework. ## Project Showcase - [AI00 RWKV Server](https://github.com/Ai00-X/ai00_server): An inference API server based on the RWKV model. - [Salvo Admin](https://github.com/lyqgit/salvo-admin): A Rust rapid development framework based on Salvo and Ruoyi-Vue3. - [Salvo Admin](https://github.com/feihua/salvo-admin): An RBAC permission management system based on Salvo and rbatis. - [Geospatial Web](https://gitlab.com/geospatialweb/rust-mvt-postgis): Rust REST API - Martin MVT Tile Server - PostGIS. - [ALLEY](https://github.com/alley-rs/alley-transfer): Software for fast file transfer between terminals on the same network segment. - [musicbot](https://github.com/AdrienPensart/musicbot): A Swiss Army knife for music. - [Replex](https://github.com/lostb1t/replex): Reshape your Plex recommendations. - [Pure Rust Instant Message (PRIM)](https://github.com/SuanCaiYv/prim): An instant messaging system implemented purely in Rust. - [rblog](https://github.com/prabirshrestha/rblog): A blog engine written in Rust. - [myblog](https://github.com/driftluo/myblog): A personal blog project. - [opensound](https://github.com/opensound-org/opensound): A professional audio system engine using Salvo as the default HTTP server backend. - [static-api](https://github.com/josejachuf/static-api-rs): A simple application that simulates a basic REST API. - [palpo](https://github.com/palpo-matrix-server/palpo): A Rust implementation of a Matrix server. - [yiirs](https://github.com/shenghui0779/yiirs): A Rust API rapid development scaffold (Salvo & Axum). - [luxy](https://github.com/alley-rs/fluxy): Software for fast file transfer between terminals on the same network segment. - [ffxiv-best-craft](https://github.com/Tnze/ffxiv-best-craft): An FF14 crafting simulator and solver with a friendly GUI. - [System-Monitor-Server](https://github.com/SimonYen/System-Monitor-Server): A server monitoring program written in Rust. - [mvt server](https://github.com/mvt-proj/mvt-rs): A simple, high-speed vector tile server developed in Rust using the Salvo web framework. ## Tutorial Resources - [Rust Salvo Zero-Basics Tutorial](https://www.bilibili.com/video/BV1FS421N71D/): A beginner-friendly tutorial for Rust Salvo. - [Building a Vocabulary Notebook with Tera and Salvo](https://www.bilibili.com/video/BV1Kg411b75s): Building a simple vocabulary notebook web application using Tera and Salvo. - [Is salvo really the simplest rust web framework?](https://www.youtube.com/watch?v=tf9x97eTcpk): Exploring whether Salvo is the simplest Rust web framework. - [Salvo.rs - Un Framework sencillo de Backend creado en Rust](https://www.youtube.com/watch?v=HlVf4mE8V9s): Spanish tutorial: Salvo.rs, a simple backend framework created in Rust. - [rust + dioxus maybe](https://www.youtube.com/watch?v=_j9tNhWbp8g): A tutorial on Rust, Salvo, Dioxus Live View, and SQLx. - [rust + dockerfile + fly](https://www.youtube.com/watch?v=IuyQxpbxjb8): A tutorial on Rust, Salvo, Dioxus Live View, SQLx, Docker, and Fly.io. --- url: /guide/ecology/chrono.md --- # Chrono: Rust Date and Time Library [Chrono](https://docs.rs/chrono/latest/chrono/) aims to provide all functionality needed to do correct operations on dates and times in the proleptic Gregorian calendar: - The `DateTime` type is timezone-aware by default, with separate timezone-naive types. - Operations that may produce an invalid or ambiguous date and time return `Option` or `MappedLocalTime`. - Configurable parsing and formatting with a strftime-inspired date and time formatting syntax. - The `Local` timezone works with the current local timezone of the OS. - Types and operations are implemented to be reasonably efficient. - To avoid increasing the binary size, Chrono does not ship with timezone data by default. Use the companion crates `Chrono-TZ` or `tzfile` for full timezone support. ## Features [Chrono](https://docs.rs/chrono/latest/chrono/) supports various runtime environments and operating systems, with several features that can be enabled or disabled. ### Default Features: - `alloc`: Enables features that depend on allocation (primarily string formatting). - `std`: Enables features that depend on the standard library. This is a superset of `alloc`, adding interoperation with standard library types and traits. - `clock`: Enables reading the local timezone (`Local`). This is a superset of `now`. - `now`: Enables reading the system time (`now`). - `wasmbind`: Provides an interface to the JS Date API for the wasm32 target. ### Optional Features: - `serde`: Enables serialization/deserialization via serde. - `rkyv`: Deprecated, use the `rkyv-*` features. - `rkyv-16`, `rkyv-32`, `rkyv-64`: Enables serialization/deserialization via rkyv, using 16-bit, 32-bit, or 64-bit integers respectively. - `rkyv-validation`: Enables rkyv validation support using bytecheck. - `arbitrary`: Constructs arbitrary instances of types with the Arbitrary crate. - `unstable-locales`: Enables localization. This adds various methods with the `_localized` suffix. ## Overview ### Time Difference/Duration Chrono provides the `TimeDelta` type to represent the magnitude of a time span. This is an "exact" duration represented in seconds and nanoseconds, and does not represent "nominal" components such as days or months. The `TimeDelta` type was previously named `Duration` (still available as a type alias). A notable difference from the similar `core::time::Duration` is that it is a signed value rather than unsigned. ### Date and Time Chrono provides the `DateTime` type to represent a date and time in a timezone. `DateTime` is timezone-aware and must be constructed from a `TimeZone` object, which defines how the local date is converted to and from UTC. There are three well-known `TimeZone` implementations: - `Utc` specifies the UTC timezone. It is most efficient. - `Local` specifies the system local timezone. - `FixedOffset` specifies an arbitrary fixed offset timezone, such as UTC+09:00 or UTC-10:30. `DateTime` values with different `TimeZone` types are distinct and cannot be mixed, but can be converted to each other using the `DateTime::with_timezone` method. You can get the current date and time in the UTC timezone (`Utc::now()`) or in the local timezone (`Local::now()`). ```rust use chrono::prelude::*; let utc: DateTime = Utc::now(); // e.g., `2014-11-28T12:45:59.324310806Z` let local: DateTime = Local::now(); // e.g., `2014-11-28T21:45:59.324310806+09:00` ``` Additionally, you can create your own date and time: ```rust use chrono::offset::MappedLocalTime; use chrono::prelude::*; let dt = Utc.with_ymd_and_hms(2014, 7, 8, 9, 10, 11).unwrap(); // `2014-07-08T09:10:11Z` ``` ### Formatting and Parsing Formatting is done via the `format` method, which format is equivalent to the familiar strftime format. The default `to_string` method and the `{:?}` specifier also give a reasonable representation. Chrono also provides `to_rfc2822` and `to_rfc3339` methods for common formats. Chrono now also provides date formatting in almost any language without additional C libraries. This feature is available under the `unstable-locales` feature: ```rust use chrono::prelude::*; let dt = Utc.with_ymd_and_hms(2014, 11, 28, 12, 0, 9).unwrap(); assert_eq!(dt.format("%Y-%m-%d %H:%M:%S").to_string(), "2014-11-28 12:00:09"); assert_eq!(dt.format_localized("%A %e %B %Y, %T", Locale::fr_BE).to_string(), "vendredi 28 novembre 2014, 12:00:09"); ``` Parsing can be done in two ways: 1. The standard `FromStr` trait (and the `parse` method on strings) can be used to parse `DateTime`, `DateTime`, and `DateTime` values. 2. `DateTime::parse_from_str` parses a date and time with an offset and returns `DateTime`. ```rust use chrono::prelude::*; let dt = "2014-11-28T12:00:09Z".parse::>().unwrap(); let fixed_dt = DateTime::parse_from_str("2014-11-28 21:00:09 +09:00", "%Y-%m-%d %H:%M:%S %z").unwrap(); ``` ### Conversion to/from Epoch Timestamps Construct `DateTime` from a UNIX timestamp using `DateTime::from_timestamp(seconds, nanoseconds)`. Get the timestamp (in seconds) from a `DateTime` using `DateTime.timestamp`. Additionally, you can get the additional number of nanoseconds with `DateTime.timestamp_subsec_nanos`. ```rust use chrono::{DateTime, Utc}; // Construct datetime from epoch: let dt: DateTime = DateTime::from_timestamp(1_500_000_000, 0).unwrap(); assert_eq!(dt.to_rfc2822(), "Fri, 14 Jul 2017 02:40:00 +0000"); // Get epoch value from datetime: let dt = DateTime::parse_from_rfc2822("Fri, 14 Jul 2017 02:40:00 +0000").unwrap(); assert_eq!(dt.timestamp(), 1_500_000_000); ``` ## Limitations - Only the proleptic Gregorian calendar is supported (i.e., extended to support dates before the era). - Date types are limited to approximately +/- 262,000 years from the common era. - Time types are limited to nanosecond precision. - Leap seconds can be represented, but Chrono does not fully support them. --- url: /guide/ecology/error.md --- # Rust Error Handling Libraries - [thiserror](https://docs.rs/thiserror/latest/thiserror/) provides convenient derive macros for custom error types. - [snafu](https://docs.rs/snafu/latest/snafu/) is an error handling and reporting framework with context. - [anyhow](https://docs.rs/anyhow/latest/anyhow/) is a flexible error handling and reporting library. ## thiserror vs snafu ### thiserror thiserror is a lightweight library that provides derive macros to simplify error definitions. Features: - Concise syntax with low ceremony - Suitable for creating error type libraries and APIs - Typically used for creating libraries intended for others ```rust use thiserror::Error; #[derive(Error, Debug)] pub enum DataError { #[error("Database error: {0}")] DatabaseError(#[from] sqlx::Error), #[error("Validation error: {0}")] ValidationError(String), #[error("Record not found")] NotFound, } ``` ### snafu snafu provides a more comprehensive error handling framework with a focus on error context and error chaining. Features: - Encourages more precise error context addition through the "context selector" pattern - Promotes the "one error enum per module" pattern - Supports both struct and tuple-style error variants - Built-in backtrace support ```rust use snafu::{Snafu, ResultExt, Backtrace}; #[derive(Debug, Snafu)] pub enum Error { #[snafu(display("Failed to read config file {filename:?}"))] ReadConfig { filename: String, source: std::io::Error, backtrace: Backtrace, }, // Tuple style is also supported #[snafu(display("IO error"))] Io(#[snafu(source)] std::io::Error, #[snafu(backtrace)] Backtrace), } // Context selector example fn read_config(path: &str) -> Result { std::fs::read_to_string(path).context(ReadConfigSnafu { filename: path })?; // ... } ``` ### Comparison | Feature | thiserror | snafu | | ------------------ | ------------------------- | ----------------------------------- | | Syntax conciseness | More concise | More verbose | | Error context | Basic support | Rich context mechanisms | | Suitable scale | Small to medium projects | Medium to large projects | | Lines per error | \~2 lines per error | \~5 lines per error | | Error organization | Usually single error enum | Encourages module-level error enums | | Backtrace support | No built-in support | Built-in support | **Selection advice**: - **Choose thiserror** when you need simple and clear error types, especially in libraries - **Choose snafu** when you need more structured error handling, particularly in large applications ## anyhow anyhow is a different error handling library from the above two, focusing on applications rather than libraries. Features: - Designed for error handling in applications, not libraries - Provides a dynamic `anyhow::Error` type that can contain any error implementing the `Error` trait - Simplifies handling across multiple error types - No need to define custom error types ```rust use anyhow::{Context, Result}; fn main() -> Result<()> { let config = std::fs::read_to_string("config.json") .context("Failed to read config file")?; let app_config: AppConfig = serde_json::from_str(&config) .context("Invalid config format")?; // Using Result as a type alias for Result Ok(()) } ``` **anyhow vs thiserror/snafu**: - anyhow focuses on error handling during rapid application development - thiserror/snafu focus on creating precise error type hierarchies - anyhow is typically used in application code - thiserror/snafu are typically used in library code In practice, anyhow and thiserror are often used together: libraries use thiserror to define precise error types, while applications use anyhow to handle errors from various sources. --- url: /guide/ecology/jemallocator.md --- # Jemallocator: Rust jemalloc Memory Allocator Alternative :::tip The default allocator may sometimes fail to release memory promptly. It is recommended to use [jemallocator](https://docs.rs/tikv-jemallocator/latest/tikv_jemallocator/) as a global allocator to replace the default one. ::: [jemallocator](https://docs.rs/tikv-jemallocator/latest/tikv_jemallocator/) is a library linked with the jemalloc memory allocator, providing the `Jemalloc` unit type that implements the allocator API and can be set as `#[global_allocator]`. `tikv-jemallocator` is the successor project to `jemallocator`. These two crates are identical except for their names. For new projects, it is recommended to use the `tikv-xxx` version. ## jemalloc Ecosystem The jemalloc support ecosystem consists of the following crates: - **tikv-jemalloc-sys**: Builds and links to jemalloc, exposing raw C bindings to it. - **tikv-jemallocator**: Provides the `Jemalloc` type that implements the `GlobalAlloc` and `Alloc` traits. - **tikv-jemalloc-ctl**: High-level wrappers for jemalloc control and introspection APIs (the `mallctl*()` function family and `MALLCTL NAMESPACE`). ## Usage ### Adding Dependencies To use tikv-jemallocator, add it as a dependency: ```toml [dependencies] [target.'cfg(not(target_env = "msvc"))'.dependencies] tikv-jemallocator = "0.5" ``` ### Setting as Global Allocator To set `tikv_jemallocator::Jemalloc` as the global allocator, add the following code to your project: ```rust #[cfg(not(target_env = "msvc"))] use tikv_jemallocator::Jemalloc; #[cfg(not(target_env = "msvc"))] #[global_allocator] static GLOBAL: Jemalloc = Jemalloc; ``` That's it! Once this static variable is defined, jemalloc will be used for all memory allocations requested by Rust code within the same program. ## Advantages jemalloc is a general-purpose malloc implementation focusing on: - Reducing memory fragmentation - Scalability in high-concurrency scenarios - Providing rich introspection and control capabilities It is particularly useful in the following scenarios: - Long-running applications - Memory-intensive workloads - High-performance services requiring fine-grained memory management ## Compatibility Notes jemalloc does not support the MSVC target environment and is therefore unavailable when using the MSVC toolchain on Windows. This is why the `cfg(not(target_env = "msvc"))` condition is included in the example code. --- url: /guide/ecology/reqwest.md --- # Reqwest: Rust HTTP Client Library [Reqwest](https://docs.rs/reqwest/latest/reqwest/) is a high-level HTTP client library that simplifies the HTTP request handling process and provides many commonly used features: - Support for both asynchronous and blocking APIs - Handling various types of request bodies: plain text, JSON, URL-encoded forms, multipart forms - Customizable redirect policies - HTTP proxy support - TLS encryption enabled by default - Cookie management ## Basic Usage ### Making a GET Request For single requests, you can use the `get` convenience method: ```rust let body = reqwest::get("https://www.rust-lang.org") .await? .text() .await?; println!("body = {body:?}"); ``` > Note: If you plan to make multiple requests, it is better to create a `Client` and reuse it to take advantage of connection pooling. ```rust let client = reqwest::Client::new(); let res = client.get("https://www.rust-lang.org") .send() .await?; ``` ### Making a POST Request You can set the request body using the `body()` method: ```rust let client = reqwest::Client::new(); let res = client.post("http://httpbin.org/post") .body("the exact body that is sent") .send() .await?; ``` ### Form Data Sending form data is a common requirement. You can use any type that can be serialized into form data: ```rust // This will send a POST request with a body of `foo=bar&baz=quux` let params = [("foo", "bar"), ("baz", "quux")]; let client = reqwest::Client::new(); let res = client.post("http://httpbin.org/post") .form(¶ms) .send() .await?; ``` ### JSON Data You can easily send JSON data using the `json` method (requires the `json` feature): ```rust // This will send a POST request with a body of `{"lang":"rust","body":"json"}` let mut map = HashMap::new(); map.insert("lang", "rust"); map.insert("body", "json"); let client = reqwest::Client::new(); let res = client.post("http://httpbin.org/post") .json(&map) .send() .await?; ``` ### Response Handling Responses can be handled in various ways: ```rust let res = client.get("https://www.rust-lang.org").send().await?; // Get the status code let status = res.status(); // Get response headers let content_type = res.headers().get("content-type").unwrap(); // Read the response body as text let body = res.text().await?; // Or parse it as JSON let json: serde_json::Value = res.json().await?; ``` ## Advanced Features ### Redirect Policy By default, the client will automatically handle HTTP redirects, following up to 10 jumps. You can customize this behavior using `ClientBuilder`: ```rust let custom_client = reqwest::Client::builder() .redirect(reqwest::redirect::Policy::none()) // Disable redirects .build()?; ``` ### Cookie Support You can enable automatic storage and sending of session cookies via `ClientBuilder`: ```rust let client = reqwest::Client::builder() .cookie_store(true) .build()?; ``` ### Proxy Settings System proxies are enabled by default, which will look for HTTP or HTTPS proxy settings in environment variables: - `HTTP_PROXY` or `http_proxy`: proxy for HTTP connections - `HTTPS_PROXY` or `https_proxy`: proxy for HTTPS connections - `ALL_PROXY` or `all_proxy`: proxy for both types of connections You can also explicitly set a proxy via code: ```rust let proxy = reqwest::Proxy::http("https://secure.example")?; let client = reqwest::Client::builder() .proxy(proxy) .build()?; // Or disable proxies let client = reqwest::Client::builder() .no_proxy() .build()?; ``` ### TLS Configuration The client uses TLS by default to connect to HTTPS targets: ```rust // Add additional server certificates let cert = reqwest::Certificate::from_pem(&cert_bytes)?; let client = reqwest::Client::builder() .add_root_certificate(cert) .build()?; // Configure client certificates let identity = reqwest::Identity::from_pkcs12_der(&pkcs12_der, "password")?; let client = reqwest::Client::builder() .identity(identity) .build()?; ``` ### Timeout Settings You can configure timeout durations for requests: ```rust let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(10)) .build()?; ``` ## Optional Features Reqwest provides various optional features that can be enabled or disabled via Cargo features: - `http2` (enabled by default): support for HTTP/2 - `default-tls` (enabled by default): provides TLS support for HTTPS - `rustls-tls`: provides TLS functionality using rustls - `blocking`: provides a blocking client API - `json`: provides JSON serialization and deserialization functionality - `multipart`: provides multipart form functionality - `cookies`: provides cookie session support - `gzip`, `brotli`, `deflate`, `zstd`: support for various response body decompression - `socks`: provides SOCKS5 proxy support ## Blocking API When asynchronous operations are not needed, you can use the blocking API (requires the `blocking` feature): ```rust let body = reqwest::blocking::get("https://www.rust-lang.org")?.text()?; let client = reqwest::blocking::Client::new(); let res = client.post("http://httpbin.org/post") .json(&map) .send()?; ``` --- url: /guide/ecology/serde.md --- # Serde: Rust Serialization and Deserialization Framework [Serde](https://docs.rs/serde/latest/serde/) is a core library in the Rust ecosystem, providing an efficient and versatile framework for serialization and deserialization. Its name is derived from the combination of "**Ser**ialization" and "**De**serialization." ## Key Features - **Versatility**: Supports multiple data formats such as JSON, YAML, TOML, MessagePack, and more. - **Zero-Cost Abstraction**: Code generated at compile time is as efficient as handwritten code. - **Flexibility**: Allows customization of serialization and deserialization behavior. - **Strong Typing**: Leverages Rust's type system to ensure data integrity. - **Wide Adoption**: Serves as the standard library for data exchange in the Rust ecosystem. ## How It Works The core of Serde lies in its Intermediate Representation (IR) design, which divides the serialization and deserialization processes into two steps: 1. **Serialization**: Converts Rust data structures into a generic intermediate representation, then into the target format. 2. **Deserialization**: Converts input formats into the generic intermediate representation, then into Rust data structures. This design enables the addition of new data formats without modifying applications that use Serde. ## Basic Usage ### Setting Up Dependencies ```toml [dependencies] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" # Or other format libraries like serde_yaml, toml, etc. ``` ### Using Derive Macros The most common usage involves using derive macros to automatically implement serialization and deserialization traits for structs: ```rust use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize, Debug)] struct Point { x: i32, y: i32, } fn main() { let point = Point { x: 1, y: 2 }; // Convert Point to a JSON string let serialized = serde_json::to_string(&point).unwrap(); println!("Serialized result: {}", serialized); // Output: {"x":1,"y":2} // Convert the JSON string back to Point let deserialized: Point = serde_json::from_str(&serialized).unwrap(); println!("Deserialized result: {:?}", deserialized); // Output: Point { x: 1, y: 2 } } ``` ### Attribute Customization Serde provides a rich set of attributes to customize serialization behavior: ```rust #[derive(Serialize, Deserialize, Debug)] struct User { #[serde(rename = "user_id")] id: u64, #[serde(default)] name: String, #[serde(skip_serializing_if = "Option::is_none")] email: Option, #[serde(skip)] temporary_data: usize, } ``` ### Supported Data Formats Serde integrates with various data formats, each with its own crate: - **serde\_json**: JSON format - **serde\_yaml**: YAML format - **toml**: TOML format - **bincode**: Binary format - **postcard**: Space-optimized binary format - **rmp/rmp-serde**: MessagePack format - **ciborium**: CBOR format - ...and other formats ## Advanced Usage ### Manual Trait Implementation For special requirements, you can manually implement the `Serialize` and `Deserialize` traits: ```rust use serde::{Serialize, Serializer, Deserialize, Deserializer}; struct MyType { // Fields... } impl Serialize for MyType { fn serialize(&self, serializer: S) -> Result where S: Serializer, { // Custom serialization logic } } impl<'de> Deserialize<'de> for MyType { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { // Custom deserialization logic } } ``` ### Type Mapping You can create mappings between different data representations: ```rust #[derive(Serialize, Deserialize)] #[serde(remote = "chrono::DateTime")] struct DateTimeRef { #[serde(with = "chrono::serde::ts_seconds")] pub inner: chrono::DateTime, } ``` ## Learning and Resources Serde is a feature-rich library, and this article only covers the basics. To fully leverage Serde, it is recommended to: 1. Visit the [Serde official documentation](https://serde.rs/) for detailed APIs and examples. 2. Check the [GitHub repository](https://github.com/serde-rs/serde) for source code and the latest updates. ## Conclusion As a foundational library in the Rust ecosystem, Serde provides powerful and flexible tools for data exchange. By mastering Serde, you can effortlessly handle various data exchange requirements, making your applications more robust and interoperable. --- url: /guide/features/affix-state.md --- # Affix State Shared Data in Requests The Affix State middleware is used to add shared data to the Depot. To use the Affix State feature, you need to enable the `affix-state` feature in Cargo.toml. ## Feature Analysis Affix State provides a simple way to share data during request processing. It allows you to: - Inject global configurations or shared data during route configuration - Access this data via the Depot in any handler - Support any cloneable type as state data ## Comparison with Other Frameworks for Quick Concept Understanding | Framework | Language | State Management Approach | | ------------------- | ---------- | ---------------------------------------------------------------------- | | Salvo (Affix State) | Rust | Stores and accesses via Depot, supports multiple types | | Axum | Rust | Stores state via Extension, similar but with different usage | | Actix-web | Rust | Uses App Data and Web::Data to share state | | Gin | Go | Uses context.Set and context.Get to store and retrieve data | | Echo | Go | Uses context.Set and context.Get to manage shared state | | Spring | Java | Uses ApplicationContext or @Bean annotations for dependency management | | Quarkus | Java | Uses CDI and dependency injection mechanisms | | Express.js | JavaScript | Uses app.locals or req.app.locals to store global state | | Nest.js | JavaScript | Uses a dependency injection system to manage shared services | | Koa.js | JavaScript | Uses ctx.state to store request-level state | ## Common Use Cases - Database connection pool sharing - Application configuration sharing - Cache instance sharing - API client sharing - Global counters or state tracking The advantage of Affix State lies in its simplicity and flexibility, allowing easy sharing of any type of data across different routes and handlers without extensive boilerplate code. _**Example Code**_ **main.rs** ```rust file="/codes/affix-state/src/main.rs" use std::sync::Arc; use std::sync::Mutex; use salvo::prelude::*; // Configuration structure with username and password #[allow(dead_code)] #[derive(Default, Clone, Debug)] struct Config { username: String, password: String, } // State structure to hold a list of fail messages #[derive(Default, Debug)] struct State { fails: Mutex>, } #[handler] async fn hello(depot: &mut Depot) -> String { // Obtain the Config instance from the depot let config = depot.get_typed::().unwrap(); // Get custom data from the depot let custom_data = depot.get::<&str>("custom_data").unwrap(); // Obtain the shared State instance from the depot let state = depot.get_typed::>().unwrap(); // Lock the fails vector and add a new fail message let mut fails_ref = state.fails.lock().unwrap(); fails_ref.push("fail message".into()); // Format and return the response string format!("Hello World\nConfig: {config:#?}\nFails: {fails_ref:#?}\nCustom Data: {custom_data}") } #[tokio::main] async fn main() { // Initialize the tracing subscriber for logging tracing_subscriber::fmt().init(); // Create a Config instance with default username and password let config = Config { username: "root".to_string(), password: "pwd".to_string(), }; // Set up the router with state injection and custom data let router = Router::new() // Use hoop to inject middleware and data into the request context .hoop( affix_state::inject(config) // Inject a shared State instance into the request context .inject(Arc::new(State { fails: Mutex::new(Vec::new()), })) // Insert custom data into the request context .insert("custom_data", "I love this world!"), ) // Register the hello handler for the root path .get(hello) // Add an additional route for the path "/hello" with the same handler .push(Router::with_path("hello").get(hello)); // Bind the server to port 8698 and start serving let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } ``` **Cargo.toml** ```toml file="/codes/affix-state/Cargo.toml" [package] name = "example-affix-state" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true, features = ["affix-state"] } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` --- url: /guide/features/basic-auth.md --- # Basic Authentication A middleware that provides support for Basic Auth. ## Introduction to Basic Auth Basic Auth is a simple HTTP authentication mechanism that transmits a username and password via the `Authorization` header in HTTP requests. The format is `Authorization: Basic `. While simple, since credentials are only Base64-encoded rather than encrypted, it is typically used with HTTPS to ensure security. ## Comparison of Basic Auth Implementations in Common Frameworks | Framework | Language | Basic Auth Implementation | | --------------- | ---------- | -------------------------------------------------------------------------------- | | Salvo | Rust | Implemented via the `BasicAuth` middleware and custom `BasicAuthValidator` | | Express | JavaScript | Uses the `express-basic-auth` middleware package | | Spring Security | Java | Configured via `httpBasic()` and implemented with `UserDetailsService` | | ASP.NET Core | C# | Uses `.UseAuthentication()` and `AddAuthentication(AuthenticationSchemes.Basic)` | | Gin | Go | Implemented via custom middleware or using the `gin-contrib/auth` package | ## Use Cases Basic Auth is suitable for the following scenarios: 1. **Internal APIs and Tools**: Management tools and APIs used within a company 2. **Development and Testing Environments**: Quickly implement authentication without a complex login system 3. **Simple API Protection**: When a complex user management system is not required 4. **Combined with Other Security Measures**: As part of a multi-layered security architecture In Salvo, the Basic Auth middleware can be easily integrated into routes. By implementing the `BasicAuthValidator` trait, custom validation logic can be defined, offering great flexibility. ## Considerations - Always use with HTTPS to protect credential transmission - Not suitable for production environments storing sensitive information - Consider using more secure authentication methods such as JWT or OAuth for production environments **main.rs** ```rust file="/codes/basic-auth/src/main.rs" use salvo::basic_auth::{BasicAuth, BasicAuthValidator}; use salvo::prelude::*; // Custom validator implementing BasicAuthValidator trait struct Validator; impl BasicAuthValidator for Validator { // Validate username and password combination async fn validate(&self, username: &str, password: &str, _depot: &mut Depot) -> bool { username == "root" && password == "pwd" } } // Simple handler that returns "Hello" for authenticated requests #[handler] async fn hello() -> &'static str { "Hello" } // Create router with basic authentication middleware fn route() -> Router { // Initialize basic authentication handler with our validator let auth_handler = BasicAuth::new(Validator); // Apply authentication middleware to the router Router::with_hoop(auth_handler).goal(hello) } #[tokio::main] async fn main() { // Initialize logging tracing_subscriber::fmt().init(); // Bind server to port 8698 and start serving let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(route()).await; } #[cfg(test)] mod tests { use salvo::prelude::*; use salvo::test::{ResponseExt, TestClient}; #[tokio::test] async fn test_basic_auth() { // Create a service instance from our router for testing purposes let service = Service::new(super::route()); // Test case 1: Verify successful authentication with valid credentials let content = TestClient::get("http://0.0.0.0:8698/") .basic_auth("root", Some("pwd")) // Use correct username/password .send(&service) // Send the request to the service .await .take_string() // Extract response body as string .await .unwrap(); // Verify response contains expected "Hello" message assert!(content.contains("Hello")); // Test case 2: Verify authentication failure with invalid password let content = TestClient::get("http://0.0.0.0:8698/") .basic_auth("root", Some("pwd2")) // Use incorrect password .send(&service) // Send the request to the service .await .take_string() // Extract response body as string .await .unwrap(); // Verify response contains "Unauthorized" error assert!(content.contains("Unauthorized")); } } ``` **Cargo.toml** ```toml file="/codes/basic-auth/Cargo.toml" [package] name = "example-basic-auth" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true, features = ["basic-auth", "test"] } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` --- url: /guide/features/cache.md --- # Cache Middleware that provides caching functionality. The Cache middleware can cache the `StatusCode`, `Headers`, and `Body` of a `Response`. For content that has already been cached, the Cache middleware will directly send the cached content from memory to the client when processing subsequent requests. Note: This plugin does not cache `Response` objects whose `Body` is `ResBody::Stream`. If applied to such a `Response`, Cache will not process these requests, and no error will occur. ## Main Features - `CacheIssuer` provides an abstraction for generating cache keys. `RequestIssuer` is one of its implementations, allowing you to define which parts of the request URL and the request `Method` should be used to generate the cache key. You can also define your own cache key generation logic. The cache key does not necessarily have to be a string; any type that satisfies the `Hash + Eq + Send + Sync + 'static` constraints can be used as a key. - `CacheStore` provides operations for storing and retrieving data. `MokaStore` is a built-in memory-based cache implementation based on `moka`. You can also define your own implementation. - `Cache` is a struct that implements `Handler`. It also contains an internal `skipper` field, which can be used to specify requests that should skip caching. By default, it uses `MethodSkipper` to skip all requests except those with `Method::GET`. Example internal implementation code: ```rust impl Cache { pub fn new(store: S, issuer: I) -> Self { let skipper = MethodSkipper::new().skip_all().skip_get(false); Cache { store, issuer, skipper: Box::new(skipper), } } } ``` ## Quick Migration from Other Frameworks If you have used caching mechanisms in other frameworks, the following conceptual mappings will help you adapt to Salvo's caching implementation more quickly: ### Rust Framework Migration Guide - **Migrating from Actix-web**: Plugins like `actix-web-cache` in Actix-web typically need to be introduced separately, whereas caching in Salvo is part of the core library. ```rust // Actix-web cache example use actix_web_cache::Cache; App::new().wrap(Cache::new().ttl(30)) // Corresponding Salvo implementation use salvo::prelude::*; Router::new().hoop(Cache::new(MokaStore::new(100), RequestIssuer::new())) ``` ### Migration Guide for Frameworks in Other Languages - **Migrating from Go/Gin**: Gin uses a middleware pattern, which Salvo also adopts in a similar manner: ```go // Gin cache example store := persist.NewMemoryStore(time.Second * 60) router.Use(cache.CachePage(store, time.Second * 30)) ``` ```rust // Corresponding Salvo implementation let store = MokaStore::new(100).with_ttl(Duration::from_secs(30)); router.hoop(Cache::new(store, RequestIssuer::new())) ``` - **Migrating from Spring Boot**: Spring Boot's declarative caching needs to be converted to Salvo's explicit middleware configuration: ```java // Spring Boot @Cacheable(value = "books", key = "#isbn") public Book findBook(ISBN isbn) { ... } ``` ```rust // Corresponding Salvo implementation - applying cache at the route level let custom_issuer = YourCustomIssuer::new(); // Implement the CacheIssuer interface Router::with_path("books").hoop(Cache::new(MokaStore::new(100), custom_issuer)) ``` - **Migrating from Express.js**: Express's caching middleware is conceptually similar to Salvo's, but the syntax differs: ```javascript // Express.js const apicache = require('apicache'); app.use(apicache.middleware('5 minutes')); // Corresponding Salvo implementation let store = MokaStore::new(100).with_ttl(Duration::from_secs(300)); router.hoop(Cache::new(store, RequestIssuer::new())) ``` When migrating from other frameworks, pay attention to several key concepts of Salvo's caching: 1. **Cache Key Generation** - Controlled via the `CacheIssuer` interface. 2. **Cache Storage** - Implemented via the `CacheStore` interface. 3. **Cache Skipping Logic** - Customized via the `skipper` mechanism. By default, Salvo only caches GET requests, which aligns with the default behavior of most frameworks. _**Example Code**_ **main.rs** ```rust file="/codes/cache-simple/src/main.rs" use std::time::Duration; use salvo::cache::{Cache, MokaStore, RequestIssuer}; use salvo::prelude::*; use salvo::writing::Text; use time::OffsetDateTime; // Handler for serving the home page with HTML content #[handler] async fn home() -> Text<&'static str> { Text::Html(HOME_HTML) } // Handler for short-lived cached response (5 seconds) #[handler] async fn short() -> String { format!( "Hello World, my birth time is {}", OffsetDateTime::now_utc() ) } // Handler for long-lived cached response (1 minute) #[handler] async fn long() -> String { format!( "Hello World, my birth time is {}", OffsetDateTime::now_utc() ) } #[tokio::main] async fn main() { // Initialize logging system tracing_subscriber::fmt().init(); // Create cache middleware for short-lived responses (5 seconds TTL) let short_cache = Cache::new( MokaStore::builder() .time_to_live(Duration::from_secs(5)) .build(), RequestIssuer::default(), ); // Create cache middleware for long-lived responses (60 seconds TTL) let long_cache = Cache::new( MokaStore::builder() .time_to_live(Duration::from_secs(60)) .build(), RequestIssuer::default(), ); // Set up router with three endpoints: // - / : Home page // - /short : Response cached for 5 seconds // - /long : Response cached for 1 minute let router = Router::new() .get(home) .push(Router::with_path("short").hoop(short_cache).get(short)) .push(Router::with_path("long").hoop(long_cache).get(long)); // Bind server to port 8698 and start serving let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } // HTML template for the home page with links to cached endpoints static HOME_HTML: &str = r#" Cache Example

Cache Example

This examples shows how to use cache middleware.

Cache 5 seconds

Cache 1 minute

"#; ``` **Cargo.toml** ```toml file="/codes/cache-simple/Cargo.toml" [package] name = "example-cache-simple" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true, features = ["cache"] } tokio = { workspace = true, features = ["macros"] } time.workspace = true tracing.workspace = true tracing-subscriber.workspace = true ``` --- url: /guide/features/caching-headers.md --- # Caching Headers A middleware that provides support for configuring caching headers. Cache control is a crucial part of web performance optimization. By correctly setting caching headers, unnecessary network requests can be reduced, thereby improving application performance. [Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) is an HTTP response header used to specify browser caching policies. It controls who can cache responses, under what conditions, and for how long. Internally, it includes implementations of three `Handler`s: `CachingHeaders`, `Modified`, and `ETag`. `CachingHeaders` is a combination of the latter two. Under normal circumstances, `CachingHeaders` is used. - `Modified`: Provides cache validation based on the resource's last modified time. - `ETag`: Uses Entity Tags to offer a more precise resource validation mechanism. - `CachingHeaders`: Combines the above two mechanisms to provide comprehensive cache control support. _**Example Code**_ **main.rs** ```rust file="/codes/caching-headers/src/main.rs" use salvo::prelude::*; // Handler that returns a simple "Hello World" response #[handler] async fn hello() -> &'static str { "Hello World" } #[tokio::main] async fn main() { // Initialize logging system tracing_subscriber::fmt().init(); // Set up router with caching headers and compression middleware // CachingHeader must be before Compression to properly set cache control headers let router = Router::with_hoop(CachingHeaders::new()) .hoop(Compression::new().min_length(0)) // Enable compression for all responses .get(hello); // Bind server to port 8698 and start serving let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } ``` **Cargo.toml** ```toml file="/codes/caching-headers/Cargo.toml" [package] name = "example-caching-headers" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true, features = ["caching-headers", "compression"] } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` --- url: /guide/features/catch-panic.md --- # Catching Panics in Requests Catch Panic is used to capture crashes that occur during request processing in the program. For specific APIs, please [refer to the documentation](https://docs.rs/salvo_extra/latest/salvo_extra/catch_panic/index.html). > **Note**: To use `CatchPanic`, you need to enable the `catch-panic` feature in `Cargo.toml`: > > ```toml > salvo = { version = "0.95.0", features = ["catch-panic"] } > ``` ## Middleware Introduction `CatchPanic` is a middleware designed to catch panics in handlers. When a panic occurs during request processing, it captures the panic and writes a 500 Internal Server Error to the response, preventing the entire server from crashing. **Important Note**: This middleware should be used as the first middleware to ensure it can catch panics from other middlewares or handlers. ## Basic Usage ```rust use salvo_core::prelude::*; use salvo_extra::catch_panic::CatchPanic; #[handler] async fn hello() { panic!("panic error!"); } #[tokio::main] async fn main() { let router = Router::new().hoop(CatchPanic::new()).get(hello); let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } ``` ## Comparison with Other Frameworks for Quick Concept Understanding ### Axum Similar to the `catch_panic` middleware in Axum's `Tower`: ```rust use axum::{ Router, routing::get, http::StatusCode, }; use tower::ServiceBuilder; use tower_http::catch_panic::CatchPanicLayer; async fn panic_handler() -> &'static str { panic!("panic error!"); } #[tokio::main] async fn main() { let app = Router::new() .route("/", get(panic_handler)) .layer( ServiceBuilder::new() .layer(CatchPanicLayer::new()) ); axum::Server::bind(&"0.0.0.0:3000".parse().unwrap()) .serve(app.into_make_service()) .await .unwrap(); } ``` ### Gin (Go) In the Go language's Gin framework, similar to the Recovery middleware: ```go package main import ( "github.com/gin-gonic/gin" ) func main() { r := gin.Default() // Includes the Recovery middleware by default r.GET("/", func(c *gin.Context) { panic("panic error!") }) r.Run(":8080") } ``` ## Example Code **main.rs** ```rust file="/codes/catch-panic/src/main.rs" use salvo::prelude::*; // Handler that deliberately panics to demonstrate panic catching #[handler] async fn hello() { panic!("panic error!"); } #[tokio::main] async fn main() { // Initialize logging system tracing_subscriber::fmt().init(); // Set up router with CatchPanic middleware to handle panics gracefully // This prevents the server from crashing when a panic occurs in a handler let router = Router::new().hoop(CatchPanic::new()).get(hello); // Bind server to port 8698 and start serving let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } ``` **Cargo.toml** ```toml file="/codes/catch-panic/Cargo.toml" [package] name = "example-catch-panic" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] anyhow = "1" salvo = { workspace = true, features = ["catch-panic"] } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` --- url: /guide/features/compression.md --- # Response Compression A middleware for compressing the content of `Response`. Supports three compression formats: `br`, `gzip`, and `deflate`. The priority and configuration of each compression method can be customized as needed. ## Data Compression in HTTP Protocol 1. File format level: Specific file types use dedicated compression algorithms. 2. HTTP protocol level: End-to-end resource compression transmission is achieved through content encoding. 3. Connection level: Data compression is performed between nodes of an HTTP connection. For more information about HTTP compression, refer to the [Mozilla Developer Network documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Compression). ## Example Code **main.rs** ```rust file="/codes/compression/src/main.rs" use salvo::prelude::*; #[tokio::main] async fn main() { // Initialize logging system tracing_subscriber::fmt().init(); // Print current working directory for debugging println!("current_dir: {:?}", std::env::current_dir().unwrap()); // Set up base directory for static files let current_dir = std::env::current_dir().unwrap(); let base_dir = if !current_dir.to_str().unwrap().ends_with("compression") { current_dir .join("compression/static") .canonicalize() .unwrap() } else { current_dir.join("static").canonicalize().unwrap() }; println!("Base Dir: {base_dir:?}"); // Configure router with different compression settings for different paths let router = Router::new() // WebSocket chat with forced compression priority .push( Router::with_hoop(Compression::new().force_priority(true)) .path("ws_chat") .get(StaticFile::new(base_dir.join("ws_chat.txt"))), ) // SSE chat with Brotli compression .push( Router::with_hoop(Compression::new().enable_brotli(CompressionLevel::Fastest)) .path("sse_chat") .get(StaticFile::new(base_dir.join("sse_chat.txt"))), ) // Todos with Zstd compression .push( Router::with_hoop(Compression::new().enable_zstd(CompressionLevel::Fastest)) .path("todos") .get(StaticFile::new(base_dir.join("todos.txt"))), ) // All other paths with Gzip compression .push( Router::with_hoop(Compression::new().enable_gzip(CompressionLevel::Fastest)) .path("{*path}") .get(StaticDir::new(base_dir)), ); // Bind server to port 8698 and start serving let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } ``` **Cargo.toml** ```toml file="/codes/compression/Cargo.toml" [package] name = "example-compression" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true, features = ["compression", "serve-static"] } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` --- url: /guide/features/concurrency-limiter.md --- # Concurrency Limitation The Concurrency Limiter middleware can control the number of concurrent requests. For specific API details, please [refer to the documentation](https://docs.rs/salvo_extra/latest/salvo_extra/concurrency_limiter/index.html). ## Application Scenarios The concurrency limiter is particularly useful in the following scenarios: - **High-Traffic Website Protection**: Limits the number of simultaneously processed requests during traffic surges to prevent server overload. - **Resource-Intensive Operation Control**: Restricts concurrency for resource-intensive operations such as file uploads and database writes. - **Third-Party API Call Limitation**: Controls concurrent request numbers when calling external services with rate limits. ## Main Functions - **Prevent Server Overload**: Maintains server stability during traffic peaks by limiting concurrent requests. - **Enhance System Reliability**: Avoids system crashes caused by resource exhaustion. - **Optimize Resource Allocation**: Ensures critical services have sufficient resources to handle requests. ## Usage Simply add the `max_concurrency(n)` middleware to your route, where `n` is the maximum allowed concurrent requests. Requests exceeding this limit will receive a `503 Service Unavailable` response. _**Example Code**_ **main.rs** ```rust file="/codes/concurrency-limiter/src/main.rs" use std::fs::create_dir_all; use std::path::Path; use salvo::prelude::*; // Handler for serving the index page with upload forms #[handler] async fn index(res: &mut Response) { res.render(Text::Html(INDEX_HTML)); } // Handler for processing file uploads with a simulated delay #[handler] async fn upload(req: &mut Request, res: &mut Response) { // Extract file from the multipart form data let file = req.file("file").await; // Simulate a long-running operation (10 seconds) tokio::time::sleep(tokio::time::Duration::from_secs(10)).await; if let Some(file) = file { // Generate destination path for the uploaded file let dest = format!("temp/{}", file.name().unwrap_or("file")); tracing::debug!(dest = %dest, "upload file"); // Copy file to destination if let Err(e) = std::fs::copy(file.path(), Path::new(&dest)) { res.status_code(StatusCode::INTERNAL_SERVER_ERROR); res.render(Text::Plain(format!("file not found in request: {e}"))); } else { res.render(Text::Plain(format!("File uploaded to {dest}"))); } } else { res.status_code(StatusCode::BAD_REQUEST); res.render(Text::Plain("file not found in request")); } } #[tokio::main] async fn main() { // Initialize logging system tracing_subscriber::fmt().init(); // Create temporary directory for file uploads create_dir_all("temp").unwrap(); // Configure router with two upload endpoints: // - /limited: Only allows one concurrent upload (with concurrency limiter) // - /unlimit: Allows unlimited concurrent uploads let router = Router::new() .get(index) .push( Router::new() .hoop(max_concurrency(1)) // Limit concurrent requests to 1 .path("limited") .post(upload), ) .push(Router::with_path("unlimit").post(upload)); // Bind server to port 8698 and start serving let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } // HTML template for the upload forms page static INDEX_HTML: &str = r#" Upload file

Upload file

Unlimit

Limited

"#; ``` **Cargo.toml** ```toml file="/codes/concurrency-limiter/Cargo.toml" [package] name = "example-concurrency-limiter" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true, features = ["concurrency-limiter"] } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` --- url: /guide/features/cors.md --- # Cross-Origin Control CORS (Cross-Origin Resource Sharing) is a mechanism that allows browsers to make requests to cross-origin servers, thereby overcoming the restrictions imposed by the browser's same-origin policy. ## What is the Same-Origin Policy? The same-origin policy is a security feature of browsers that restricts how documents or scripts loaded from one origin can interact with resources from another origin. "Same origin" refers to having the same protocol, domain, and port number. ## Why is CORS Needed? CORS support is required when a frontend application needs to access APIs from a different origin. For example: - The frontend application is deployed at `https://frontend.com` - The API service is deployed at `https://api.backend.com` Without CORS, the browser will prevent the frontend application from accessing the API service. ## How CORS Works CORS implements cross-origin access control through a series of HTTP headers: - **Simple Requests**: Sent directly, with the server controlling whether access is allowed via response headers. - **Preflight Requests**: The browser first sends an OPTIONS request to ask the server if cross-origin access is allowed, and only sends the actual request after receiving permission. Since the browser sends preflight requests with `Method::OPTIONS`, it is necessary to handle such requests. The CORS middleware must be added to the `Service`. ## Using CORS in Salvo {/* add cors in Salvo with cargo */} :::info this is how to install cors in Salvo ```rust cargo add salvo --features cors ``` ::: Salvo provides built-in CORS middleware that can be easily configured and used. Here is an example code snippet: :::warning Note: The `.hoop(cors)` middleware is applied to the `Service`, not the `Router`. This ensures automatic handling of OPTIONS preflight requests. ```rust let cors = Cors::new() .allow_origin(["http://127.0.0.1:8698", "http://localhost:8698"]) .allow_methods(vec![Method::GET, Method::POST, Method::DELETE]) .allow_headers("authorization") .into_handler(); // Set up backend router with CORS protection let router = Router::with_path("hello").post(hello); let service = Service::new(router).hoop(cors); ``` ::: _**Example Code**_ **main.rs** ```rust file="/codes/cors/src/main.rs" use salvo::cors::Cors; use salvo::http::Method; use salvo::prelude::*; #[tokio::main] async fn main() { // Initialize logging system tracing_subscriber::fmt().init(); // Start both backend and frontend servers concurrently tokio::join!(backend_server(), frontend_server()); } async fn backend_server() { // Handler that returns a simple message for CORS demonstration #[handler] async fn hello() -> &'static str { "hello, I am content from remote server." } // Configure CORS middleware with specific settings: // - Allow requests from localhost:8698 // - Allow specific HTTP methods // - Allow authorization header let cors = Cors::new() .allow_origin(["http://127.0.0.1:8698", "http://localhost:8698"]) .allow_methods(vec![Method::GET, Method::POST, Method::DELETE]) .allow_headers("authorization") .into_handler(); // Set up backend router with CORS protection let router = Router::with_path("hello").post(hello); let service = Service::new(router).hoop(cors); // Start backend server on port 5600 let acceptor = TcpListener::new("0.0.0.0:5600").bind().await; Server::new(acceptor).serve(service).await; } async fn frontend_server() { // Handler that serves the HTML page with CORS test #[handler] async fn index() -> Text<&'static str> { Text::Html(HTML_DATA) } // Set up frontend router to serve the test page let router = Router::new().get(index); // Start frontend server on port 8698 let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } // HTML template with JavaScript code to test CORS // Contains a button that triggers a POST request to the backend server const HTML_DATA: &str = r#" Salvo Cors
"#; ``` **Cargo.toml** ```toml file="/codes/cors/Cargo.toml" [package] name = "example-cors" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true, features = ["cors"] } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` ## Main Configuration Options The CORS middleware provides various configuration options: - **Allowed Origins**: Controls which domains can access resources. - **Allowed Methods**: Specifies allowed HTTP methods (GET, POST, etc.). - **Allowed Headers**: Specifies allowed request headers. - **Exposed Headers**: Specifies which response headers can be accessed by the client. - **Allow Credentials**: Determines whether requests can include credentials such as cookies. - **Preflight Request Cache Time**: The cache duration for preflight request results. By configuring CORS appropriately, you can ensure both security and the ability to meet cross-origin access requirements. --- url: /guide/features/craft.md --- # Craft Feature Craft allows developers to automatically generate handler functions and endpoints through simple annotations, while seamlessly integrating with OpenAPI documentation generation. ## Use Cases The Craft feature is particularly useful in the following scenarios: - When you need to quickly create route handler functions from struct methods - When you want to reduce boilerplate code for manual parameter extraction and error handling - When you need to automatically generate OpenAPI documentation for your API - When you want to decouple business logic from the web framework ## Basic Usage To use the Craft feature, you need to import the following modules: ```rust use salvo::oapi::extract::*; use salvo::prelude::*; ``` ### Creating a Service Struct Annotate your impl block with the `#[craft]` macro to convert struct methods into handler functions or endpoints. ```rust #[derive(Clone)] pub struct Opts { state: i64, } #[craft] impl Opts { // Constructor fn new(state: i64) -> Self { Self { state } } // More methods... } ``` ### Creating Handler Functions Use `#[craft(handler)]` to convert a method into a handler function: ```rust #[craft(handler)] fn add1(&self, left: QueryParam, right: QueryParam) -> String { (self.state + *left + *right).to_string() } ``` This method will become a handler function that: - Automatically extracts `left` and `right` values from query parameters - Accesses the `state` from the struct - Returns the calculation result as a string response ### Creating Endpoints Use `#[craft(endpoint)]` to convert a method into an endpoint: ```rust #[craft(endpoint)] pub(crate) fn add2( self: ::std::sync::Arc, left: QueryParam, right: QueryParam, ) -> String { (self.state + *left + *right).to_string() } ``` Endpoints can utilize `Arc` to share state, which is particularly useful when handling concurrent requests. ### Static Endpoints You can also create static endpoints that don't depend on instance state: ```rust #[craft(endpoint(responses((status_code = 400, description = "Wrong request parameters."))))] pub fn add3(left: QueryParam, right: QueryParam) -> String { (*left + *right).to_string() } ``` In this example, a custom error response description is added, which will be reflected in the generated OpenAPI documentation. ## Parameter Extractors Salvo's `oapi::extract` module provides various parameter extractors, with the most common including: - `QueryParam`: Extracts parameters from query strings - `PathParam`: Extracts parameters from URL paths - `FormData`: Extracts parameters from form data - `JsonBody`: Extracts parameters from JSON request bodies These extractors automatically handle parameter parsing and type conversion, greatly simplifying the writing of handler functions. ## Integration with OpenAPI The Craft feature can automatically generate API documentation compliant with the OpenAPI specification. In the example: ```rust let router = Router::new() .push(Router::with_path("add1").get(opts.add1())) .push(Router::with_path("add2").get(opts.add2())) .push(Router::with_path("add3").get(Opts::add3())); // Generate OpenAPI documentation let doc = OpenApi::new("Example API", "0.0.1").merge_router(&router); // Add OpenAPI documentation and Swagger UI routes let router = router .push(doc.into_router("/api-doc/openapi.json")) .push(SwaggerUi::new("/api-doc/openapi.json").into_router("swagger-ui")); ``` With this configuration, the API documentation will be available at the `/api-doc/openapi.json` endpoint, and Swagger UI will be accessible at the `/swagger-ui` path. ## Complete Example Below is a complete example demonstrating how to use the Craft feature to create three different types of endpoints: **main.rs** ```rust file="/codes/craft/src/main.rs" use salvo::oapi::extract::*; use salvo::prelude::*; use std::sync::Arc; // Options struct holding a state value for calculations #[derive(Clone)] pub struct Opts { state: i64, } // Implement methods for Opts using the craft macro for API generation #[craft] impl Opts { // Constructor for Opts fn new(state: i64) -> Self { Self { state } } // Handler method that adds state value to two query parameters #[craft(handler)] fn add1(&self, left: QueryParam, right: QueryParam) -> String { (self.state + *left + *right).to_string() } // Endpoint method using Arc for shared state #[craft(endpoint)] pub(crate) fn add2( self: ::std::sync::Arc, left: QueryParam, right: QueryParam, ) -> String { (self.state + *left + *right).to_string() } // Static endpoint method with custom error response #[craft(endpoint(responses((status_code = 400, description = "Wrong request parameters."))))] pub fn add3(left: QueryParam, right: QueryParam) -> String { (*left + *right).to_string() } } #[tokio::main] async fn main() { // Create shared state with initial value 1 let opts = Arc::new(Opts::new(1)); // Configure router with three endpoints: // - /add1: Uses instance method with state // - /add2: Uses Arc-wrapped instance method // - /add3: Uses static method without state let router = Router::new() .push(Router::with_path("add1").get(opts.add1())) .push(Router::with_path("add2").get(opts.add2())) .push(Router::with_path("add3").get(Opts::add3())); // Generate OpenAPI documentation let doc = OpenApi::new("Example API", "0.0.1").merge_router(&router); // Add OpenAPI documentation and Swagger UI routes let router = router .push(doc.into_router("/api-doc/openapi.json")) .push(SwaggerUi::new("/api-doc/openapi.json").into_router("swagger-ui")); // Start server on localhost:8698 let acceptor = TcpListener::new("127.0.0.1:8698").bind().await; Server::new(acceptor).serve(router).await; } ``` **Cargo.toml** ```toml file="/codes/craft/Cargo.toml" [package] name = "example-craft" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true, features = ["craft", "oapi"] } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` --- url: /guide/features/csrf.md --- # CSRF Protection ## What is CSRF? CSRF (Cross-Site Request Forgery) is a web security vulnerability where attackers trick authenticated users into performing unintended actions without their knowledge. In simple terms, attackers exploit a user's logged-in status to send malicious requests on their behalf. ## How CSRF Attacks Work The typical attack process involves: 1. The user logs into the target website A and obtains an authentication cookie. 2. The user visits a malicious website B. 3. Code on malicious website B automatically sends a request to website A, carrying the user's cookie. 4. Website A cannot distinguish whether this is a malicious request or a legitimate user action. ## Protection Strategies Salvo provides CSRF middleware to protect your applications from such attacks: - Add a hidden CSRF token to forms. - Verify that user-submitted requests contain a valid CSRF token. - By default, validate POST, PATCH, DELETE, and PUT requests. ## Common Token Rotation Strategies CSRF token systems commonly use one of two rotation strategies: 1. **Per-session**: Reuse the same token until it expires or is removed from the trusted store. This works well with page refreshes, multiple tabs, and browser back or forward navigation. 2. **Per-request**: Issue a fresh token after every accepted request. This shortens the lifetime of each token, but clients must always submit the latest token from the most recent response. In Salvo, token rotation is configured separately from storage: - Storage decides where the trusted token lives: `CookieStore` or `SessionStore`. - Rotation decides how often the token changes: `CsrfRotationPolicy::PerSession` or `CsrfRotationPolicy::PerRequest`. Salvo defaults to `CsrfRotationPolicy::PerSession`. If you want stricter request-level rotation, opt into `PerRequest` explicitly: ```rust let csrf = bcrypt_cookie_csrf(FormFinder::new("csrf_token")) .rotation_policy(CsrfRotationPolicy::PerRequest); ``` ## CSRF Implementation in Salvo `Csrf` is a struct that implements the `Handler` trait and includes an internal `skipper` field to specify requests that should skip validation. By default, it validates `Method::POST`, `Method::PATCH`, `Method::DELETE`, and `Method::PUT` requests. Salvo supports two CSRF token storage methods. Storage and rotation are independent choices: 1. **CookieStore**: Stores the token in a cookie and verifies whether the `csrf-token` in the request headers or form matches the cookie value. 2. **SessionStore**: Stores the token in the session and requires the use of session middleware. _**Example Code**_ (cookie store) **main.rs** ```rust file="/codes/csrf-cookie-store/src/main.rs" use salvo::csrf::*; use salvo::prelude::*; use serde::{Deserialize, Serialize}; // Handler for serving the home page with links to different CSRF protection methods #[handler] pub async fn home(res: &mut Response) { let html = r#" Csrf CookieStore

Csrf Example: CookieStore

"#; res.render(Text::Html(html)); } // Handler for GET requests that displays a form with the current CSRF token #[handler] pub async fn get_page(depot: &mut Depot, res: &mut Response) { let csrf_token = depot.csrf_token().unwrap_or_default(); res.render(Text::Html(get_page_html(csrf_token, ""))); } // Handler for POST requests that processes form submission with CSRF validation #[handler] pub async fn post_page(req: &mut Request, depot: &mut Depot, res: &mut Response) { // Define data structure for form submission #[derive(Deserialize, Serialize, Debug)] struct Data { csrf_token: String, message: String, } // Parse the submitted form data into the Data struct let data = req.parse_form::().await.unwrap(); // Log the received form data for debugging tracing::info!("posted data: {:?}", data); // Read the token that should be embedded into the response form. let csrf_token = depot.csrf_token().unwrap_or_default(); // Generate HTML response with the current token and display the submitted data let html = get_page_html(csrf_token, &format!("{data:#?}")); // Send the HTML response back to the client res.render(Text::Html(html)); } #[tokio::main] async fn main() { // Initialize logging system tracing_subscriber::fmt().init(); // Configure CSRF token finder in form data let form_finder = FormFinder::new("csrf_token"); // Initialize different CSRF protection methods let bcrypt_csrf = bcrypt_cookie_csrf(form_finder.clone()); let hmac_csrf = hmac_cookie_csrf(*b"01234567012345670123456701234567", form_finder.clone()); let aes_gcm_cookie_csrf = aes_gcm_cookie_csrf(*b"01234567012345670123456701234567", form_finder.clone()); let ccp_cookie_csrf = ccp_cookie_csrf(*b"01234567012345670123456701234567", form_finder.clone()); // Configure router with different CSRF protection endpoints let router = Router::new() .get(home) // Bcrypt-based CSRF protection .push( Router::with_hoop(bcrypt_csrf) .path("bcrypt") .get(get_page) .post(post_page), ) // HMAC-based CSRF protection .push( Router::with_hoop(hmac_csrf) .path("hmac") .get(get_page) .post(post_page), ) // AES-GCM-based CSRF protection .push( Router::with_hoop(aes_gcm_cookie_csrf) .path("aes_gcm") .get(get_page) .post(post_page), ) // ChaCha20Poly1305-based CSRF protection .push( Router::with_hoop(ccp_cookie_csrf) .path("ccp") .get(get_page) .post(post_page), ); // Start server on port 8698 let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } // Helper function to generate HTML page with CSRF token and message fn get_page_html(csrf_token: &str, msg: &str) -> String { format!( r#" Csrf Example

Csrf Example: CookieStore

{msg}
"# ) } ``` **Cargo.toml** ```toml file="/codes/csrf-cookie-store/Cargo.toml" [package] name = "example-csrf-cookie-store" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true, features = ["csrf"] } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true serde = { workspace = true, features = ["derive"] } ``` _**Example Code**_ (session store) **main.rs** ```rust file="/codes/csrf-session-store/src/main.rs" use salvo::csrf::*; use salvo::prelude::*; use serde::{Deserialize, Serialize}; // Handler for serving the home page with links to different CSRF protection methods #[handler] pub async fn home(res: &mut Response) { let html = r#" Csrf SessionStore

Csrf Example: SessionStore

"#; res.render(Text::Html(html)); } // Handler for GET requests that displays a form with the current CSRF token #[handler] pub async fn get_page(depot: &mut Depot, res: &mut Response) { let csrf_token = depot.csrf_token().unwrap_or_default(); res.render(Text::Html(get_page_html(csrf_token, ""))); } // Handler for POST requests that processes form submission with CSRF validation #[handler] pub async fn post_page(req: &mut Request, depot: &mut Depot, res: &mut Response) { // Define data structure for form submission #[derive(Deserialize, Serialize, Debug)] struct Data { csrf_token: String, message: String, } // Parse the submitted form data into the Data struct let data = req.parse_form::().await.unwrap(); // Log the received form data for debugging tracing::info!("posted data: {:?}", data); // Read the token that should be embedded into the response form. let csrf_token = depot.csrf_token().unwrap_or_default(); // Generate HTML response with the current token and display the submitted data let html = get_page_html(csrf_token, &format!("{data:#?}")); // Send the HTML response back to the client res.render(Text::Html(html)); } #[tokio::main] async fn main() { // Initialize logging system tracing_subscriber::fmt().init(); // Configure CSRF token finder in form data let form_finder = FormFinder::new("csrf_token"); // Initialize different CSRF protection methods using session store let bcrypt_csrf = bcrypt_session_csrf(form_finder.clone()); let hmac_csrf = hmac_session_csrf(*b"01234567012345670123456701234567", form_finder.clone()); let aes_gcm_session_csrf = aes_gcm_session_csrf(*b"01234567012345670123456701234567", form_finder.clone()); let ccp_session_csrf = ccp_session_csrf(*b"01234567012345670123456701234567", form_finder.clone()); // Configure session handler with memory store and secret key let session_handler = salvo::session::SessionHandler::builder( salvo::session::MemoryStore::new(), b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab", ) .build() .unwrap(); // Configure router with session handler and different CSRF protection endpoints let router = Router::new() .get(home) .hoop(session_handler) // Bcrypt-based CSRF protection .push( Router::with_hoop(bcrypt_csrf) .path("bcrypt") .get(get_page) .post(post_page), ) // HMAC-based CSRF protection .push( Router::with_hoop(hmac_csrf) .path("hmac") .get(get_page) .post(post_page), ) // AES-GCM-based CSRF protection .push( Router::with_hoop(aes_gcm_session_csrf) .path("aes_gcm") .get(get_page) .post(post_page), ) // ChaCha20Poly1305-based CSRF protection .push( Router::with_hoop(ccp_session_csrf) .path("ccp") .get(get_page) .post(post_page), ); // Start server on port 8698 let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } // Helper function to generate HTML page with CSRF token and message fn get_page_html(csrf_token: &str, msg: &str) -> String { format!( r#" Csrf SessionStore

Csrf Example: SessionStore

{msg}
"# ) } ``` **Cargo.toml** ```toml file="/codes/csrf-session-store/Cargo.toml" [package] name = "example-csrf-session-store" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true, features = ["csrf", "session"] } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true serde = { workspace = true, features = ["derive"] } ``` --- url: /guide/features/flash.md --- # Flash A middleware that provides Flash Message functionality. `FlashStore` offers data storage and retrieval operations. `CookieStore` stores data in `Cookies`, while `SessionStore` stores data in `Session`. `SessionStore` must be used in conjunction with the `session` feature. _**Example Code**_ ### Cookie Storage Example **main.rs** ```rust file="/codes/flash-cookie-store/src/main.rs" use std::fmt::Write; use salvo::flash::{CookieStore, FlashDepotExt}; use salvo::prelude::*; #[handler] pub async fn set_flash(depot: &mut Depot, res: &mut Response) { let flash = depot.outgoing_flash_mut(); flash.info("Hey there!").debug("How is it going?"); res.render(Redirect::other("/get")); } #[handler] pub async fn get_flash(depot: &mut Depot, _res: &mut Response) -> String { let mut body = String::new(); if let Some(flash) = depot.incoming_flash() { for message in flash.iter() { writeln!(body, "{} - {}", message.value, message.level).unwrap(); } } body } #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); let router = Router::new() .hoop(CookieStore::new().into_handler()) .push(Router::with_path("get").get(get_flash)) .push(Router::with_path("set").get(set_flash)); let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } ``` **Cargo.toml** ```toml file="/codes/flash-cookie-store/Cargo.toml" [package] name = "example-flash-cookie-store" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true, features = ["flash"] } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` ### Session Storage Example **main.rs** ```rust file="/codes/flash-session-store/src/main.rs" use std::fmt::Write; use salvo::flash::{FlashDepotExt, SessionStore}; use salvo::prelude::*; #[handler] pub async fn set_flash(depot: &mut Depot, res: &mut Response) { let flash = depot.outgoing_flash_mut(); flash.info("Hey there!").debug("How is it going?"); res.render(Redirect::other("/get")); } #[handler] pub async fn get_flash(depot: &mut Depot, _res: &mut Response) -> String { let mut body = String::new(); if let Some(flash) = depot.incoming_flash() { for message in flash.iter() { writeln!(body, "{} - {}", message.value, message.level).unwrap(); } } body } #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); let session_handler = salvo::session::SessionHandler::builder( salvo::session::MemoryStore::new(), b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab", ) .build() .unwrap(); let router = Router::new() .hoop(session_handler) .hoop(SessionStore::new().into_handler()) .push(Router::with_path("get").get(get_flash)) .push(Router::with_path("set").get(set_flash)); let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } ``` **Cargo.toml** ```toml file="/codes/flash-session-store/Cargo.toml" [package] name = "example-flash-session-store" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true, features = ["flash", "session"] } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` --- url: /guide/features/force-https.md --- # Force HTTPS The `force-https` middleware can redirect all requests to use the HTTPS protocol. If this middleware is applied to a `Router`, it will only enforce protocol redirection when a route is matched. If a page does not exist, no redirection will occur. However, a more common requirement is to automatically redirect any request, even when the route fails to match and returns a `404` error. In such cases, the middleware can be added to the `Service`. Middleware added to the `Service` will always execute, regardless of whether the request is successfully matched by a route. _**Example Code**_ **main.rs** ```rust file="/codes/force-https/src/main.rs" use salvo::conn::rustls::{Keycert, RustlsConfig}; use salvo::prelude::*; #[handler] async fn hello() -> &'static str { "Hello World" } #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); let router = Router::new().get(hello); let service = Service::new(router).hoop(ForceHttps::new().https_port(5443)); let config = RustlsConfig::new( Keycert::new() .cert(include_bytes!("../certs/cert.pem").as_ref()) .key(include_bytes!("../certs/key.pem").as_ref()), ); let acceptor = TcpListener::new("0.0.0.0:5443") .rustls(config) .join(TcpListener::new("0.0.0.0:8698")) .bind() .await; Server::new(acceptor).serve(service).await; } ``` **Cargo.toml** ```toml file="/codes/force-https/Cargo.toml" [package] name = "example-force-https" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true, features = ["rustls", "force-https"] } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` --- url: /guide/features/hello-h3.md --- # HTTP/3 Support Salvo provides support for HTTP/3, which can be enabled via the `quinn` feature. HTTP/3 is based on the QUIC protocol and offers lower latency and better performance compared to traditional HTTP/1.1 and HTTP/2, especially in unstable network environments. ## Enabling HTTP/3 Support To enable HTTP/3 support in Salvo, you need to activate the `quinn` feature in your `Cargo.toml` file: ```toml salvo = { workspace = true, features = ["quinn"] } ``` ## HTTP/3 Use Cases HTTP/3 is particularly suitable for the following scenarios: - Applications on mobile devices and in unstable network environments - Real-time applications requiring low latency - Scenarios involving parallel downloads of numerous small files - Applications needing connection migration (e.g., seamless switching from WiFi to cellular networks without connection interruption) ## Example Code The following is a simple HTTP/3 server example that supports both HTTP/3 (QUIC) and HTTPS (TCP): ```rust use salvo::conn::rustls::{Keycert, RustlsConfig}; use salvo::prelude::*; // Handler function that responds with "Hello World" #[handler] async fn hello() -> &'static str { "Hello World" } #[tokio::main] async fn main() { // Initialize the logging system tracing_subscriber::fmt().init(); // Load TLS certificate and private key from embedded PEM files let cert = include_bytes!("../certs/cert.pem").to_vec(); let key = include_bytes!("../certs/key.pem").to_vec(); // Create a router and add an endpoint let router = Router::new().get(hello); // Configure TLS settings using Rustls let config = RustlsConfig::new(Keycert::new().cert(cert.as_slice()).key(key.as_slice())); // Create a TLS-encrypted TCP listener on port 8698 let listener = TcpListener::new(("0.0.0.0", 8698)).rustls(config.clone()); // Create a QUIC listener and combine it with the TCP listener let acceptor = QuinnListener::new(config.build_quinn_config().unwrap(), ("0.0.0.0", 8698)) .join(listener) .bind() .await; // Start the server supporting both HTTP/3 (QUIC) and HTTPS (TCP) Server::new(acceptor).serve(router).await; } ``` ## Key Code Analysis ### TLS Configuration ```rust // Configure TLS settings using Rustls let config = RustlsConfig::new(Keycert::new().cert(cert.as_slice()).key(key.as_slice())); ``` Since HTTP/3 is based on the QUIC protocol, which requires TLS 1.3 for encryption, TLS certificates and keys must be configured. In Salvo, we use `RustlsConfig` to configure TLS. ### Combining Listeners ```rust // Create a TLS-encrypted TCP listener let listener = TcpListener::new(("0.0.0.0", 8698)).rustls(config.clone()); // Create a QUIC listener and combine it with the TCP listener let acceptor = QuinnListener::new(config.build_quinn_config().unwrap(), ("0.0.0.0", 8698)) .join(listener) .bind() .await; ``` This code is the core part of handling HTTP/3 in Salvo. It first creates a TLS-enabled TCP listener (for HTTP/1.1 and HTTP/2), then creates a QUIC listener (for HTTP/3). The `join` method combines these two listeners, enabling the server to handle requests from different protocols simultaneously. ## Running the Example To run this example, you need valid TLS certificates and private keys. In a development environment, self-signed certificates can be used. The complete example code can be found in the [Salvo GitHub repository](https://github.com/salvo-rs/salvo/tree/main/examples/hello-h3). Note that since many clients do not yet fully support HTTP/3, it is essential for this server to support both HTTP/3 and HTTPS. ## Considerations 1. HTTP/3 requires TLS 1.3 support, so valid certificates and keys must be configured. 2. Clients must support the HTTP/3 protocol to utilize this feature; otherwise, they will fall back to HTTP/1.1 or HTTP/2. 3. In production environments, certificates issued by a trusted Certificate Authority (CA) should be used instead of self-signed certificates. --- url: /guide/features/index.md --- # Features ## Features ### [HTTP/3 Support](/guide/features/hello-h3.md) - [Enabling HTTP/3 Support](/guide/features/hello-h3.md#enabling-http3-support) - [HTTP/3 Use Cases](/guide/features/hello-h3.md#http3-use-cases) - [Example Code](/guide/features/hello-h3.md#example-code) - [Key Code Analysis](/guide/features/hello-h3.md#key-code-analysis) - [Running the Example](/guide/features/hello-h3.md#running-the-example) - [Considerations](/guide/features/hello-h3.md#considerations) ### [Affix State Shared Data in Requests](/guide/features/affix-state.md) - [Feature Analysis](/guide/features/affix-state.md#feature-analysis) - [Comparison with Other Frameworks for Quick Concept Understanding](/guide/features/affix-state.md#comparison-with-other-frameworks-for-quick-concept-understanding) - [Common Use Cases](/guide/features/affix-state.md#common-use-cases) ### [Basic Authentication](/guide/features/basic-auth.md) - [Introduction to Basic Auth](/guide/features/basic-auth.md#introduction-to-basic-auth) - [Comparison of Basic Auth Implementations in Common Frameworks](/guide/features/basic-auth.md#comparison-of-basic-auth-implementations-in-common-frameworks) - [Use Cases](/guide/features/basic-auth.md#use-cases) - [Considerations](/guide/features/basic-auth.md#considerations) ### [Cache](/guide/features/cache.md) - [Main Features](/guide/features/cache.md#main-features) - [Quick Migration from Other Frameworks](/guide/features/cache.md#quick-migration-from-other-frameworks) ### [Caching Headers](/guide/features/caching-headers.md) ### [Catching Panics in Requests](/guide/features/catch-panic.md) - [Middleware Introduction](/guide/features/catch-panic.md#middleware-introduction) - [Basic Usage](/guide/features/catch-panic.md#basic-usage) - [Comparison with Other Frameworks for Quick Concept Understanding](/guide/features/catch-panic.md#comparison-with-other-frameworks-for-quick-concept-understanding) - [Example Code](/guide/features/catch-panic.md#example-code) ### [Response Compression](/guide/features/compression.md) - [Data Compression in HTTP Protocol](/guide/features/compression.md#data-compression-in-http-protocol) - [Example Code](/guide/features/compression.md#example-code) ### [Concurrency Limitation](/guide/features/concurrency-limiter.md) - [Application Scenarios](/guide/features/concurrency-limiter.md#application-scenarios) - [Main Functions](/guide/features/concurrency-limiter.md#main-functions) - [Usage](/guide/features/concurrency-limiter.md#usage) ### [Cross-Origin Control](/guide/features/cors.md) - [What is the Same-Origin Policy?](/guide/features/cors.md#what-is-the-same-origin-policy) - [Why is CORS Needed?](/guide/features/cors.md#why-is-cors-needed) - [How CORS Works](/guide/features/cors.md#how-cors-works) - [Using CORS in Salvo](/guide/features/cors.md#using-cors-in-salvo) - [Main Configuration Options](/guide/features/cors.md#main-configuration-options) ### [CSRF Protection](/guide/features/csrf.md) - [What is CSRF?](/guide/features/csrf.md#what-is-csrf) - [How CSRF Attacks Work](/guide/features/csrf.md#how-csrf-attacks-work) - [Protection Strategies](/guide/features/csrf.md#protection-strategies) - [Common Token Rotation Strategies](/guide/features/csrf.md#common-token-rotation-strategies) - [CSRF Implementation in Salvo](/guide/features/csrf.md#csrf-implementation-in-salvo) ### [Flash](/guide/features/flash.md) ### [Force HTTPS](/guide/features/force-https.md) ### [JWT Authentication](/guide/features/jwt-auth.md) - [Choosing a crypto provider](/guide/features/jwt-auth.md#choosing-a-crypto-provider) ### [Logging Middleware](/guide/features/logging.md) ### [OpenTelemetry Integration](/guide/features/open-telemetry.md) ### [OpenAPI Documentation Generation](/guide/features/openapi.md) - [Data Extractors](/guide/features/openapi.md#data-extractors) - [`#[endpoint]`](/guide/features/openapi.md#endpoint) - [ToSchema](/guide/features/openapi.md#toschema) - [ToParameters](/guide/features/openapi.md#toparameters) - [Best Practices for Automatic Documentation](/guide/features/openapi.md#best-practices-for-automatic-documentation) - [Swagger UI Authentication](/guide/features/openapi.md#swagger-ui-authentication) ### [Reverse Proxy](/guide/features/proxy.md) - [Path forwarding and normalization](/guide/features/proxy.md#path-forwarding-and-normalization) ### [Rate Limiting](/guide/features/rate-limiter.md) - [Key Features](/guide/features/rate-limiter.md#key-features) ### [Request Chain ID](/guide/features/request-id.md) ### [Static Server](/guide/features/serve-static.md) - [Use Cases](/guide/features/serve-static.md#use-cases) - [Main Features](/guide/features/serve-static.md#main-features) - [Pros and Cons of rust-embed](/guide/features/serve-static.md#pros-and-cons-of-rust-embed) ### [Session](/guide/features/session.md) - [What is Session](/guide/features/session.md#what-is-session) - [Using Session in Salvo](/guide/features/session.md#using-session-in-salvo) - [Common Use Cases](/guide/features/session.md#common-use-cases) ### [File Upload Size Limiter Middleware](/guide/features/size-limiter.md) - [Use Cases](/guide/features/size-limiter.md#use-cases) ### [Server-Sent Events (SSE)](/guide/features/sse.md) - [What is SSE](/guide/features/sse.md#what-is-sse) - [Chat Application Example](/guide/features/sse.md#chat-application-example) - [Integration with Large Language Models](/guide/features/sse.md#integration-with-large-language-models) ### [Third-Party WebSocket Plugin](/guide/features/third-party.md) - [salvo-websocket](/guide/features/third-party.md#salvo-websocket) ### [Timeout Middleware](/guide/features/timeout.md) - [Use Cases](/guide/features/timeout.md#use-cases) ### [Tower Middleware Compatibility](/guide/features/tower-compat.md) - [Core Concepts of Tower](/guide/features/tower-compat.md#core-concepts-of-tower) - [Salvo Compatibility Support](/guide/features/tower-compat.md#salvo-compatibility-support) ### [Trailing Slash](/guide/features/trailing-slash.md) - [Use Cases](/guide/features/trailing-slash.md#use-cases) ### [WebSocket](/guide/features/websocket.md) - [Introduction to WebSocket](/guide/features/websocket.md#introduction-to-websocket) ### [WebTransport](/guide/features/webtransport.md) - [Use Cases](/guide/features/webtransport.md#use-cases) - [Salvo Implementation](/guide/features/webtransport.md#salvo-implementation) - [Complete Example](/guide/features/webtransport.md#complete-example) ### [Craft Feature](/guide/features/craft.md) - [Use Cases](/guide/features/craft.md#use-cases) - [Basic Usage](/guide/features/craft.md#basic-usage) - [Parameter Extractors](/guide/features/craft.md#parameter-extractors) - [Integration with OpenAPI](/guide/features/craft.md#integration-with-openapi) - [Complete Example](/guide/features/craft.md#complete-example) --- url: /guide/features/jwt-auth.md --- # JWT Authentication JWT (JSON Web Token) is an open standard (RFC 7519) for securely transmitting information between parties. It is a compact, URL-safe method of representing claims in JSON object format, commonly used for authentication and information exchange. A JWT consists of three parts: - **Header** - Specifies the token type and the signing algorithm used - **Payload** - Contains claims, such as user ID, roles, expiration time, etc. - **Signature** - Used to verify that the message was not altered during transmission Typical usage of JWT in authentication flow: 1. After user login, the server generates a JWT token 2. The token is returned to the client and stored (typically in localStorage or cookies) 3. In subsequent requests, the client includes this token in the Authorization header 4. The server validates the token's authenticity and grants access Provides middleware for JWT Auth authentication. ## Choosing a crypto provider Salvo 0.95 provides two alternative JWT features: - `jwt-auth` uses the AWS-LC provider. - `jwt-auth-ring` uses the RustCrypto provider. Enable only one in normal builds. The separate `ring` feature selects ring for Salvo's rustls integration; it does not enable JWT authentication. The example below uses `jwt-auth`. To use RustCrypto instead: ```toml salvo = { version = "0.95", default-features = false, features = ["server", "http1", "ring", "jwt-auth-ring"] } ``` If Cargo feature unification enables both JWT providers, call `salvo::jwt_auth::install_crypto_provider()` at the very start of `main`, before any direct `jsonwebtoken` operation or JWT middleware setup. The example below does this so provider selection remains deterministic. _**Example Code**_ **main.rs** ```rust file="/codes/jwt-auth/src/main.rs" use jsonwebtoken::{self, EncodingKey}; use salvo::http::{Method, StatusError}; use salvo::jwt_auth::{ConstDecoder, QueryFinder}; use salvo::prelude::*; use serde::{Deserialize, Serialize}; use time::{Duration, OffsetDateTime}; const SECRET_KEY: &str = "YOUR SECRET_KEY"; #[derive(Serialize, Deserialize, Clone, Debug)] pub struct JwtClaims { username: String, exp: i64, } #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); salvo::jwt_auth::install_crypto_provider() .expect("install the JWT crypto provider before first use"); let auth_handler: JwtAuth = JwtAuth::new(ConstDecoder::from_secret(SECRET_KEY.as_bytes())) .finders(vec![ // Box::new(HeaderFinder::new()), Box::new(QueryFinder::new("jwt_token")), // Box::new(CookieFinder::new("jwt_token")), ]) .force_passed(true); let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor) .serve(Router::with_hoop(auth_handler).goal(index)) .await; } #[handler] async fn index(req: &mut Request, depot: &mut Depot, res: &mut Response) -> anyhow::Result<()> { if req.method() == Method::POST { let (username, password) = ( req.form::("username").await.unwrap_or_default(), req.form::("password").await.unwrap_or_default(), ); if !validate(&username, &password) { res.render(Text::Html(LOGIN_HTML)); return Ok(()); } let exp = OffsetDateTime::now_utc() + Duration::days(14); let claim = JwtClaims { username, exp: exp.unix_timestamp(), }; let token = jsonwebtoken::encode( &jsonwebtoken::Header::default(), &claim, &EncodingKey::from_secret(SECRET_KEY.as_bytes()), )?; res.render(Redirect::other(format!("/?jwt_token={token}"))); } else { match depot.jwt_auth_state() { JwtAuthState::Authorized => { let data = depot.jwt_auth_data::().unwrap(); res.render(Text::Plain(format!( "Hi {}, have logged in successfully!", data.claims.username ))); } JwtAuthState::Unauthorized => { res.render(Text::Html(LOGIN_HTML)); } JwtAuthState::Forbidden => { res.render(StatusError::forbidden()); } } } Ok(()) } fn validate(username: &str, password: &str) -> bool { username == "root" && password == "pwd" } static LOGIN_HTML: &str = r#" JWT Auth Demo

JWT Auth

"#; ``` **Cargo.toml** ```toml file="/codes/jwt-auth/Cargo.toml" [package] name = "example-jwt-auth" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] anyhow = "1" jsonwebtoken = { workspace = true } salvo = { workspace = true, features = ["anyhow", "jwt-auth"] } serde = { workspace = true } time = { workspace = true } tokio = { workspace = true, features = ["macros"] } tracing = { workspace = true } tracing-subscriber = { workspace = true } ``` --- url: /guide/features/logging.md --- # Logging Middleware Logging plays a crucial role in web applications, as it enables: - Providing detailed information about the request handling process to help developers track application behavior - Assisting in troubleshooting and debugging, especially in production environments - Monitoring application performance and resource usage - Recording user access patterns and system exceptions - Meeting security auditing and compliance requirements Salvo provides a middleware with basic logging functionality. If the middleware is added directly to a `Router`, it will not capture 404 errors returned when no `Router` matches the request. It is recommended to add it to the `Service` instead. _**Example Code**_ **main.rs** ```rust file="/codes/logging/src/main.rs" use salvo::logging::Logger; use salvo::prelude::*; #[handler] async fn hello() -> &'static str { "Hello World" } #[handler] async fn error() -> StatusError { StatusError::bad_request() .brief("Bad request error") .detail("The request was malformed.") } #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); let router = Router::new() .get(hello) .push(Router::with_path("error").get(error)); let service = Service::new(router).hoop(Logger::new()); let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(service).await; } ``` **Cargo.toml** ```toml file="/codes/logging/Cargo.toml" [package] name = "example-logging" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true, features = ["logging"] } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` --- url: /guide/features/open-telemetry.md --- # OpenTelemetry Integration OpenTelemetry is an open-source observability framework that provides a standardized approach to collecting and exporting telemetry data from applications, including traces, metrics, and logs. It helps developers monitor application performance, troubleshoot issues, and understand the behavior of distributed systems. Key Advantages of OpenTelemetry: - Unified APIs and tools with no vendor lock-in - Support for multiple programming languages and backend systems - Low-overhead data collection - Robust support for distributed tracing Provides middleware with OpenTelemetry support. You can refer to the [example](https://github.com/salvo-rs/salvo/tree/main/examples/otel-jaeger) in the official repository. --- url: /guide/features/openapi.md --- # OpenAPI Documentation Generation OpenAPI is an open-source specification for describing RESTful API interface designs. It defines API request and response structures, parameters, return types, error codes, and other details in JSON or YAML format, making communication between client and server more explicit and standardized. OpenAPI was originally the open-source version of the Swagger specification and has now become an independent project supported by many large enterprises and developers. Using the OpenAPI specification helps development teams collaborate better, reduce communication costs, and improve development efficiency. Additionally, OpenAPI provides developers with tools for automatically generating API documentation, mock data, and test cases, facilitating development and testing work. Salvo provides OpenAPI integration (modified from [utoipa](https://github.com/juhaku/utoipa)). Salvo elegantly extracts relevant OpenAPI data type information automatically from `Handler` based on its own characteristics. Salvo also integrates several popular open-source OpenAPI interfaces such as SwaggerUI, Scalar, RapiDoc, and ReDoc. Since Rust type names can be long and not always suitable for OpenAPI usage, `salvo-oapi` provides the `Namer` type, which allows customizing rules to change type names in OpenAPI as needed. {/* add tips for intall oapi with cargo */} :::tip This is tips for install Oapi in Salvo ```bash cargo add salvo --features oapi ``` ::: _**Example Code**_ **main.rs** ```rust file="/codes/oapi-hello/src/main.rs" use salvo::oapi::extract::*; use salvo::prelude::*; #[endpoint] async fn hello(name: QueryParam) -> String { format!("Hello, {}!", name.as_deref().unwrap_or("World")) } #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); let router = Router::new().push(Router::with_path("hello").get(hello)); let doc = OpenApi::new("test api", "0.0.1").merge_router(&router); let router = router .unshift(doc.into_router("/api-doc/openapi.json")) .unshift(SwaggerUi::new("/api-doc/openapi.json").into_router("/swagger-ui")); let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } ``` **Cargo.toml** ```toml file="/codes/oapi-hello/Cargo.toml" [package] name = "example-oapi-hello" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true, features = ["oapi"] } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` Enter `http://localhost:8698/swagger-ui` in your browser to see the Swagger UI page. The OpenAPI integration in Salvo is quite elegant. For the example above, compared to a normal Salvo project, we just did the following steps: - Enable the `oapi` feature in `Cargo.toml`: `salvo = { workspace = true, features = ["oapi"] }`; - Replace `#[handler]` with `#[endpoint]`; - Use `name: QueryParam` to get the value of the query string. When you visit `http://localhost/hello?name=chris`, the `name` query string will be parsed. The `false` in `QueryParam` means that this parameter is optional. If you visit `http://localhost/hello`, it will not report an error. On the contrary, if it is `QueryParam`, it means that this parameter must be provided, otherwise an error will be returned. - Create `OpenAPI` and the corresponding `Router`. The `merge_router` in `OpenApi::new("test api", "0.0.1").merge_router(&router)` means that this `OpenAPI` obtains the necessary document information by parsing a certain route and its sub-routes. Some `Handler`s of routes may not provide information for generating documents, and these routes will be ignored, such as `Handler`s defined using the `#[handler]` macro instead of the `#[endpoint]` macro. That is to say, in actual projects, for reasons such as development progress, you can choose not to generate OpenAPI documents, or partially generate OpenAPI documents. Subsequently, you can gradually increase the number of OpenAPI interfaces generated, and all you need to do is change `#[handler]` to `#[endpoint]` and modify the function signature. ## Data Extractors You can import preset common data extractors through `use salvo::oapi::extract::*;`. The extractor will provide some necessary information to Salvo so that Salvo can generate OpenAPI documents. - `QueryParam`: An extractor for extracting data from query strings. `QueryParam` means that this parameter is not required and can be omitted. `QueryParam` means that this parameter is required and cannot be omitted. If it is not provided, an error will be returned; - `HeaderParam`: An extractor for extracting data from the request header. `HeaderParam` means that this parameter is not required and can be omitted. `HeaderParam` means that this parameter is required and cannot be omitted. If it is not provided, an error will be returned; - `CookieParam`: An extractor for extracting data from the request cookie. `CookieParam` means that this parameter is not required and can be omitted. `CookieParam` means that this parameter is required and cannot be omitted. If it is not provided, an error will be returned; - `PathParam`: An extractor for extracting path parameters from the request URL. If this parameter does not exist, the route matching will not be successful, so there is no case where it can be omitted; - `FormBody`: Extracts information from the submitted form of the request; - `JsonBody`: Extracts information from the JSON-formatted payload submitted by the request; ## `#[endpoint]` When generating OpenAPI documents, you need to use the `#[endpoint]` macro instead of the regular `#[handler]` macro. It is actually an enhanced version of the `#[handler]` macro. - It can obtain the necessary information for generating OpenAPI through the function signature; - For information that is not convenient to provide through the signature, you can directly provide it by adding attributes in the `#[endpoint]` macro. The information provided in this way will be merged with the information obtained through the function signature. If there is a conflict, it will overwrite the information provided by the function signature. You can use Rust's built-in `#[deprecated]` attribute to mark a Handler as obsolete. Although the `#[deprecated]` attribute supports adding information such as the reason for deprecation and the version, OpenAPI does not support it, so this information will be ignored when generating OpenAPI. The doc comment part in the code will be automatically extracted to generate OpenAPI. The first line is used to generate _`summary`_, and the entire comment part will be used to generate _`description`_. ```rust /// This is a summary of the operation /// /// All lines of the doc comment will be included to operation description. #[endpoint] fn endpoint() {} ``` ## ToSchema You can use `#[derive(ToSchema)]` to define data structures: ```rust #[derive(ToSchema)] struct Pet { id: u64, name: String, } ``` `#[derive(ToSchema)]` also supports enum types. Here are various enum usage patterns: _**Simple Enum**_ — When all variants are unit types, generates a string enum: ```rust use salvo_oapi::ToSchema; use serde::{Serialize, Deserialize}; /// Pet kind #[derive(Serialize, Deserialize, ToSchema)] #[serde(rename_all = "snake_case")] enum PetKind { /// Dog Dog, /// Cat Cat, /// Bird Bird, } ``` Generated OpenAPI schema: ```json { "type": "string", "enum": ["dog", "cat", "bird"] } ``` _**Complex Enum**_ — When variants contain data, uses `oneOf` by default: ```rust use salvo_oapi::ToSchema; use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize, ToSchema)] #[serde(rename_all = "snake_case")] enum PaymentMethod { /// Cash payment Cash, /// Card payment Card { card_number: String, cvv: String }, /// E-wallet Wallet(String), } ``` _**Tagged Enums**_ — Supports `tag`, `tag` + `content`, `untagged` serde enum representations: ```rust use salvo_oapi::ToSchema; use serde::{Serialize, Deserialize}; // Internally tagged #[derive(Serialize, Deserialize, ToSchema)] #[serde(tag = "type")] enum Event { Click { x: i32, y: i32 }, KeyPress { key: String }, } // Adjacently tagged #[derive(Serialize, Deserialize, ToSchema)] #[serde(tag = "type", content = "data")] enum Message { Text(String), Image { url: String, width: u32 }, } ``` _**Using enum fields in structs**_ : ```rust use salvo_oapi::ToSchema; use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize, ToSchema)] #[serde(rename_all = "snake_case")] enum Status { Active, Inactive, Pending, } #[derive(Serialize, Deserialize, ToSchema)] struct User { name: String, status: Status, } ``` You can use `#[salvo(schema(...))]` to define optional settings: - `example = ...` can be `json!(...)`. `json!(...)` will be parsed by `serde_json::json!` as `serde_json::Value`. ```rust #[derive(ToSchema)] #[salvo(schema(example = json!({"name": "bob the cat", "id": 0})))] struct Pet { id: u64, name: String, } ``` - `xml(...)` can be used to define Xml object properties: ```rust #[derive(ToSchema)] struct Pet { id: u64, #[salvo(schema(xml(name = "pet_name", prefix = "u")))] name: String, } ``` ## ToParameters Generate [path parameters][path_parameters] from the fields of the structure. This is the `#[derive]` implementation of the [`ToParameters`][to_parameters] trait. Usually, path parameters need to be defined in [`#[salvo_oapi::endpoint(...parameters(...))]`][path_parameters] of the endpoint. However, when using a [`struct`][struct] to define parameters, the above steps can be omitted. Nevertheless, if you need to give a description or change the default configuration, then [`primitive types`][primitive] and [`String`][std_string] path parameters or \[tuple] style path parameters still need to be defined in `parameters(...)`. You can use Rust's built-in `#[deprecated]` attribute to mark fields as deprecated, which will be reflected in the generated OpenAPI specification. The `#[deprecated]` attribute supports adding extra information such as the reason for deprecation or the version from which it is deprecated, but OpenAPI does not support it. OpenAPI only supports a boolean value to determine whether it is deprecated. Although it is perfectly possible to declare a deprecation with a reason, such as `#[deprecated = "There is better way to do this"]`, this reason will not be presented in the OpenAPI specification. The comment document on the structure field will be used as the parameter description in the generated OpenAPI specification. ```rust #[derive(salvo_oapi::ToParameters, serde::Deserialize)] struct Query { /// Query todo items by name. name: String } ``` ### ToParameters Container Attributes for `#[salvo(parameters(...))]` The following attributes can be used in the container attribute `#[salvo(parameters(…))]` of the structure derived from `ToParameters` - `names(...)` Defines a comma-separated list of names for the unnamed fields of the structure used as path parameters. Only supported on unnamed structures. - `style = ...` Defines the serialization method for all parameters, specified by [`ParameterStyle`][style]. The default value is based on the _`parameter_in`_ attribute. - `default_parameter_in = ...` Defines the default position used by the parameters of this field. The value of this position comes from [`parameter::ParameterIn`][in_enum]. If this attribute is not provided, the default is from `query`. - `rename_all = ...` can be used as an alternative to `serde`'s `rename_all`. It actually provides the same functionality. Use `names` to define a name for a single unnamed parameter. ```rust # use salvo_oapi::ToParameters; #[derive(ToParameters, serde::Deserialize)] #[salvo(parameters(names("id")))] struct Id(u64); ``` Use `names` to define names for multiple unnamed parameters. ```rust # use salvo_oapi::ToParameters; #[derive(ToParameters, serde::Deserialize)] #[salvo(parameters(names("id", "name")))] struct IdAndName(u64, String); ``` ### ToParameters Field Attributes for `#[salvo(parameter(...))]` The following attributes can be used on structure fields `#[salvo(parameter(...))]`: - `style = ...` Defines how parameters are serialized by [`ParameterStyle`][style]. The default value is based on the _`parameter_in`_ attribute. - `parameter_in = ...` Use the value from [`parameter::ParameterIn`][in_enum] to define where this field parameter is. If this value is not provided, the default is from `query`. - `explode` Defines whether to create a new _`parameter=value`_ pair for each parameter in _`object`_ or _`array`_. - `allow_reserved` Defines whether reserved characters _`:/?#[]@!$&'()*+,;=`_ are allowed in the parameter value. - `example = ...` can be a method reference or _`json!(...)`_. The given example will override any examples of the underlying parameter type. - `value_type = ...` can be used to override the default type used by fields in the OpenAPI specification. This is useful when the default type does not correspond to the actual type, for example, when using third-party types not defined in [`ToSchema`][to_schema] or [`primitive` types][primitive]`. The value can be any Rust type that can be serialized to JSON under normal circumstances, or a custom type like _`Object`_._`Object\`\_ that will be rendered as a generic OpenAPI object. - `inline` If enabled, the definition of this field type must come from [`ToSchema`][to_schema], and this definition will be inlined. - `default = ...` can be a method reference or _`json!(...)`_. - `format = ...` can be a variant of the [`KnownFormat`][known_format] enum, or an open value in string form. By default, the format is inferred from the type of the attribute according to the OpenApi specification. - `write_only` Defines that the attribute is only used for **write** operations _POST,PUT,PATCH_ and not _GET_. - `read_only` Defines that the attribute is only used for **read** operations _GET_ and not _POST,PUT,PATCH_. - `nullable` Defines whether the attribute can be `null` (note that this is different from not required). - `required = ...` is used to force the parameter to be required. [See rules](https://docs.rs/salvo-oapi/latest/salvo_oapi/derive.ToParameters.html#field-nullability-and-required-rules). - `rename = ...` can be used as an alternative to `serde`'s `rename`. It actually provides the same functionality. - `multiple_of = ...` is used to define a multiple of the value. The parameter value is considered valid only when the parameter value is divided by the value of this keyword and the result is an integer. The multiple value must be strictly greater than _`0`_. - `maximum = ...` is used to define the upper limit of the value, including the current value. - `minimum = ...` is used to define the lower limit of the value, including the current value. - `exclusive_maximum = ...` is used to define the upper limit of the value, excluding the current value. - `exclusive_minimum = ...` is used to define the lower limit of the value, excluding the current value. - `max_length = ...` is used to define the maximum length of a `string` type value. - `min_length = ...` is used to define the minimum length of a `string` type value. - `pattern = ...` is used to define a valid regular expression that the field value must match. The regular expression adopts the _ECMA-262_ version. - `max_items = ...` can be used to define the maximum number of items allowed in an `array` type field. The value must be a non-negative integer. - `min_items = ...` can be used to define the minimum number of items allowed in an `array` type field. The value must be a non-negative integer. - `with_schema = ...` uses a function reference to create a _`schema`_ instead of the default _`schema`_. The function must satisfy the definition`fn() -> Into>`. It does not receive any parameters and must return any value that can be converted to `RefOr`. - `additional_properties = ...` is used to define free-form types for `map`, such as [`HashMap`](https://doc.rust-lang.org/std/collections/hash_map/struct.HashMap.html) and [`BTreeMap`](https://doc.rust-lang.org/std/collections/struct.BTreeMap.html). Free-form types allow any type to be used in map values. Supported formats are _`additional_properties`_ and _`additional_properties = true`_. #### Field nullability and required rules Some of the rules for nullability and requiredness of _`ToParameters`_ field attributes can also be used for _`ToSchema`_ field attributes. [See rules](https://docs.rs/salvo-oapi/latest/salvo_oapi/derive.ToSchema.html#field-nullability-and-required-rules). ### Partial `#[serde(...)]` attributes support The ToParameters derive currently supports some [serde attributes][serde attributes]. These supported attributes will be reflected in the generated OpenAPI documentation. The following attributes are currently supported: - `rename_all = "..."` is supported at the container level. - `rename = "..."` is **only** supported at the field level. - `default` is supported at the container and field level according to [serde attributes][serde attributes]. - `skip_serializing_if = "..."` is **only** supported at the field level. - `with = ...` is **only** supported at the field level. - `skip_serializing = "..."` is **only** supported at the field or variant level. - `skip_deserializing = "..."` is **only** supported at the field or variant level. - `skip = "..."` is **only** supported at the field level. Other _`serde`_ attributes will affect serialization, but will not be reflected in the generated OpenAPI documentation. ### Examples _**Demonstrates the use of the `#[salvo(parameters(...))]` container attribute in conjunction with [`ToParameters`][to_parameters] on path parameters, and inlines a query field:**_ ```rust use serde::Deserialize; use salvo_core::prelude::*; use salvo_oapi::{ToParameters, ToSchema}; #[derive(Deserialize, ToSchema)] #[serde(rename_all = "snake_case")] enum PetKind { Dog, Cat, } #[derive(Deserialize, ToParameters)] struct PetQuery { /// Name of pet name: Option, /// Age of pet age: Option, /// Kind of pet #[salvo(parameter(inline))] kind: PetKind } #[salvo_oapi::endpoint( parameters(PetQuery), responses( (status_code = 200, description = "success response") ) )] async fn get_pet(query: PetQuery) { // ... } ``` _**Use `value_type` to override the `String` type with the `i64` type.**_ ```rust # use salvo_oapi::ToParameters; #[derive(ToParameters, serde::Deserialize)] #[salvo(parameters(default_parameter_in = Query))] struct Filter { #[salvo(parameter(value_type = i64))] id: String, } ``` _**Use `value_type` to override the `String` type with the `Object` type. In the OpenAPI specification, the `Object` type will be displayed as `type:object`.**_ ```rust # use salvo_oapi::ToParameters; #[derive(ToParameters, serde::Deserialize)] #[salvo(parameters(default_parameter_in = Query))] struct Filter { #[salvo(parameter(value_type = Object))] id: String, } ``` _**You can also use a generic to override the default type of a field.**_ ```rust # use salvo_oapi::ToParameters; #[derive(ToParameters, serde::Deserialize)] #[salvo(parameters(default_parameter_in = Query))] struct Filter { #[salvo(parameter(value_type = Option))] id: String } ``` _**You can even use one \[`Vec`] to override another \[`Vec`].**_ ```rust # use salvo_oapi::ToParameters; #[derive(ToParameters, serde::Deserialize)] #[salvo(parameters(default_parameter_in = Query))] struct Filter { #[salvo(parameter(value_type = Vec))] id: Vec } ``` _**We can use another [`ToSchema`][to_schema] to override the field type.**_ ```rust # use salvo_oapi::{ToParameters, ToSchema}; #[derive(ToSchema)] struct Id { value: i64, } #[derive(ToParameters, serde::Deserialize)] #[salvo(parameters(default_parameter_in = Query))] struct Filter { #[salvo(parameter(value_type = Id))] id: String } ``` _**Example of attribute value validation**_ ```rust #[derive(salvo_oapi::ToParameters, serde::Deserialize)] struct Item { #[salvo(parameter(maximum = 10, minimum = 5, multiple_of = 2.5))] id: i32, #[salvo(parameter(max_length = 10, min_length = 5, pattern = "[a-z]*"))] value: String, #[salvo(parameter(max_items = 5, min_items = 1))] items: Vec, } ``` _**Use `schema_with` to manually implement the schema for the field.**_ ```rust # use salvo_oapi::schema::Object; fn custom_type() -> Object { Object::new() .schema_type(salvo_oapi::SchemaType::String) .format(salvo_oapi::SchemaFormat::Custom( "email".to_string(), )) .description("this is the description") } #[derive(salvo_oapi::ToParameters, serde::Deserialize)] #[salvo(parameters(default_parameter_in = Query))] struct Query { #[salvo(parameter(schema_with = custom_type))] email: String, } ``` - `rename_all = ...`: Supports a syntax similar to `serde` to define rules for renaming fields. If both `#[serde(rename_all = "...")]` and `#[salvo(schema(rename_all = "..."))]` are defined, `#[serde(rename_all = "...")]` is preferred. - `symbol = ...`: A string literal used to define the name path of the structure on the line in OpenAPI. For example, `#[salvo(schema(symbol = "path.to.Pet"))]`. - `default`: You can use the `Default` implementation of the structure to fill all fields with default values. ## Best Practices for Automatic Documentation ### Use Function Parameter Extractors Instead of Manual Extraction **IMPORTANT**: For OpenAPI documentation to be generated automatically, use function parameter extractors instead of manually extracting from `Request`. ```rust // ❌ BAD: Manual extraction - NO OpenAPI documentation generated #[endpoint] async fn get_user(req: &mut Request) -> Json { let id = req.param::("id").unwrap(); // Not documented! let page = req.query::("page"); // Not documented! // ... } // ✅ GOOD: Parameter extractors - OpenAPI documentation auto-generated #[endpoint] async fn get_user( id: PathParam, // Documented as path parameter page: QueryParam, // Documented as optional query parameter ) -> Json { // ... } ``` When you use extractors like `PathParam`, `QueryParam`, `JsonBody`, the OpenAPI generator can: - Automatically detect parameter names and types - Mark parameters as required or optional - Generate proper schema definitions ### Return Result with impl Writer Types **IMPORTANT**: Return `Result` where both `T` and `E` implement `Writer` and `EndpointOutRegister`. This enables automatic documentation of both success and error responses. ```rust // ❌ LIMITED: Only success response documented #[endpoint] async fn get_user(id: PathParam) -> Json { Json(User { /* ... */ }) } // ✅ BETTER: Both success and error responses documented #[endpoint] async fn get_user(id: PathParam) -> Result, StatusError> { let id = id.into_inner(); if id <= 0 { return Err(StatusError::bad_request().brief("Invalid ID")); } Ok(Json(User { id, name: "John".into(), email: "john@example.com".into() })) } ``` ### Summary of Documentation Requirements | Pattern | Parameters Documented | Response Documented | | --------------------------------- | --------------------- | ------------------- | | `req.param()` / `req.query()` | ❌ No | - | | `PathParam` / `QueryParam` | ✅ Yes | - | | `-> Json` | - | ✅ Success only | | `-> Result, StatusError>` | - | ✅ Success + Error | | `-> Result, CustomError>` | - | ✅ Full control | ## Swagger UI Authentication If you want protected endpoints to appear in Swagger UI's **Authorize** dialog, you need to wire the OpenAPI metadata explicitly: - Register a security scheme with `OpenApi::add_security_scheme(...)`. - Attach a security requirement with `#[endpoint(security(...))]` or, if the route is protected by middleware, with router metadata such as `.oapi_security(...)`. - Optionally pass Swagger UI OAuth client settings with `SwaggerUi::oauth(...)`. _**Example Code**_ **main.rs** ```rust file="/codes/oapi-oauth2-password/src/main.rs" use jsonwebtoken::{EncodingKey, Header}; use salvo::jwt_auth::{ConstDecoder, HeaderFinder}; use salvo::oapi::extract::*; use salvo::oapi::security::{Flow, OAuth2, Password, Scopes}; use salvo::oapi::swagger_ui::oauth; use salvo::oapi::{SecurityRequirement, SecurityScheme}; use salvo::prelude::*; use serde::{Deserialize, Serialize}; use time::{Duration, OffsetDateTime}; const SECRET_KEY: &[u8] = b"salvo-oauth2-password-demo"; const PROFILE_SCOPE: &str = "profile"; const SECURITY_SCHEME_NAME: &str = "oauth2_password"; #[derive(Clone, Debug, Serialize, Deserialize)] struct Claims { sub: String, exp: i64, } #[derive(Debug, Serialize, Deserialize, ToSchema)] struct PasswordGrantForm { grant_type: Option, username: String, password: String, scope: Option, client_id: Option, client_secret: Option, } #[derive(Debug, Serialize, Deserialize, ToSchema)] struct TokenResponse { access_token: String, token_type: String, expires_in: i64, } #[derive(Debug, Serialize, Deserialize, ToSchema)] struct UserProfile { username: String, scope: String, } #[endpoint] async fn issue_token( form: FormBody, ) -> Result, StatusError> { let form = form.into_inner(); if form .grant_type .as_deref() .is_some_and(|grant_type| grant_type != "password") { return Err(StatusError::bad_request()); } if form.username != "root" || form.password != "pwd" { return Err(StatusError::unauthorized()); } let expires_in = Duration::hours(1); let claims = Claims { sub: form.username, exp: (OffsetDateTime::now_utc() + expires_in).unix_timestamp(), }; let access_token = jsonwebtoken::encode( &Header::default(), &claims, &EncodingKey::from_secret(SECRET_KEY), ) .map_err(|_| StatusError::internal_server_error())?; Ok(Json(TokenResponse { access_token, token_type: "Bearer".into(), expires_in: expires_in.whole_seconds(), })) } #[endpoint] async fn profile(depot: &mut Depot) -> Result, StatusError> { let claims = depot .jwt_auth_data::() .ok_or_else(StatusError::unauthorized)?; Ok(Json(UserProfile { username: claims.claims.sub.clone(), scope: PROFILE_SCOPE.into(), })) } #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); salvo::jwt_auth::install_crypto_provider() .expect("install the JWT crypto provider before first use"); let auth: JwtAuth = JwtAuth::new(ConstDecoder::from_secret(SECRET_KEY)) .finders(vec![Box::new(HeaderFinder::new())]) .force_passed(true); let api = Router::with_path("api") .hoop(auth) // Middleware protects the route, but the OpenAPI security requirement // still needs to be declared explicitly for documentation. .oapi_security(SecurityRequirement::new( SECURITY_SCHEME_NAME, [PROFILE_SCOPE], )) .push(Router::with_path("profile").get(profile)); let router = Router::new() .push(Router::with_path("oauth/token").post(issue_token)) .push(api); let doc = OpenApi::new("OAuth2 Password Demo", "0.0.1") .add_security_scheme( SECURITY_SCHEME_NAME, SecurityScheme::OAuth2(OAuth2::new([Flow::Password(Password::new( "/oauth/token", Scopes::from_iter([(PROFILE_SCOPE, "Read the current user's profile")]), ))])), ) .merge_router(&router); let router = router .unshift(doc.into_router("/api-doc/openapi.json")) .unshift( SwaggerUi::new("/api-doc/openapi.json") .oauth( oauth::Config::new() .client_id("swagger-ui") .scopes(vec![PROFILE_SCOPE.into()]), ) .into_router("/swagger-ui"), ); let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } ``` **Cargo.toml** ```toml file="/codes/oapi-oauth2-password/Cargo.toml" [package] name = "example-oapi-oauth2-password" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] jsonwebtoken = { workspace = true } salvo = { workspace = true, features = ["jwt-auth", "oapi"] } serde = { workspace = true } time = { workspace = true } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` In this example, the `/api/profile` route is protected by `JwtAuth` middleware, while the OpenAPI security declaration is attached manually with `.oapi_security(...)`. Salvo does not currently infer Swagger/OpenAPI security declarations from middleware automatically. Middleware and documentation metadata are separate today, so when authentication is added through middleware you still need to declare the matching OpenAPI security scheme and requirement yourself. ### Error Handling For general applications, we will define a global error type (AppError) and implement `Writer` or `Scribe` for `AppError` so that errors can be sent to the client as web page information. For OpenAPI, in order to get the necessary error information, we also need to implement `EndpointOutRegister` for this error: ```rust use salvo::http::{StatusCode, StatusError}; use salvo::oapi::{self, EndpointOutRegister, ToSchema}; impl EndpointOutRegister for Error { fn register(components: &mut oapi::Components, operation: &mut oapi::Operation) { operation.responses.insert( StatusCode::INTERNAL_SERVER_ERROR.as_str(), oapi::Response::new("Internal server error").add_content("application/json", StatusError::to_schema(components)), ); operation.responses.insert( StatusCode::NOT_FOUND.as_str(), oapi::Response::new("Not found").add_content("application/json", StatusError::to_schema(components)), ); operation.responses.insert( StatusCode::BAD_REQUEST.as_str(), oapi::Response::new("Bad request").add_content("application/json", StatusError::to_schema(components)), ); } } ``` This error set defines all possible error messages that the entire web application may return. However, in many cases, our `Handler` may only contain a few specific error types. In this case, you can use `status_codes` to filter out the required error type information: ```rust #[endpoint(status_codes(201, 409))] pub async fn create_todo(new_todo: JsonBody) -> Result { Ok(StatusCode::CREATED) } ``` [to_schema]: https://docs.rs/salvo-oapi/latest/salvo_oapi/trait.ToSchema.html [known_format]: https://docs.rs/salvo-oapi/latest/salvo_oapi/enum.KnownFormat.html {/* [xml]: openapi/xml/struct.Xml.html */} [to_parameters]: https://docs.rs/salvo-oapi/latest/salvo_oapi/trait.ToParameters.html [path_parameters]: /guide/features/openapi.md#parameters-attributes [struct]: https://doc.rust-lang.org/std/keyword.struct.html [style]: https://docs.rs/salvo-oapi/latest/salvo_oapi/enum.ParameterStyle.html [in_enum]: https://docs.rs/salvo-oapi/latest/salvo_oapi/enum.ParameterIn.html [primitive]: https://doc.rust-lang.org/std/primitive/index.html [serde attributes]: https://serde.rs/attributes.html [std_string]: https://doc.rust-lang.org/std/string/struct.String.html --- url: /guide/features/proxy.md --- # Reverse Proxy A reverse proxy is a server architecture that receives requests from clients and forwards them to one or more backend servers. Unlike a forward proxy (which acts on behalf of clients), a reverse proxy operates on behalf of the server side. Key advantages of reverse proxies: - Load Balancing: Distributes requests across multiple servers - Enhanced Security: Hides real server information - Content Caching: Improves performance - Path Rewriting and Forwarding: Routes requests flexibly The Salvo framework provides middleware for reverse proxy functionality. ## Path forwarding and normalization When you mount a proxy with a wildcard route such as `Router::with_path("api/{**rest}")`, the default proxy path is the captured tail. For example, `/api/users` is forwarded as `/users` when the upstream is `http://backend`. If the backend also expects the `/api` prefix, include that prefix in the upstream URL: ```rust let router = Router::with_path("api/{**rest}").goal( Proxy::use_hyper_client("http://backend/api") ); ``` `Proxy` normalizes literal `.` and `..` path segments before forwarding. If the proxy is used as an access-control boundary and the backend may perform another URL decode pass, enable strict path normalization: ```rust let router = Router::with_path("api/{**rest}").goal( Proxy::use_hyper_client("http://backend/api") .strict_path_normalization(true) ); ``` Strict path normalization rejects ambiguous percent-encoded path characters such as `%2e`, `%2f`, `%5c`, and encoded percent signs that can become those sequences after another decode pass. The backend should still validate and normalize its own routes and file paths; strict mode helps avoid proxy/backend path interpretation mismatches. _**Example Code**_ **main.rs** ```rust file="/codes/proxy-simple/src/main.rs" use salvo::prelude::*; #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); // In this example, if the requested URL begins with , the proxy goes to // ; if the requested URL begins with , the proxy // goes to . let router = Router::new() .push( Router::new() .host("127.0.0.1") .path("{**rest}") .goal(Proxy::use_hyper_client("https://docs.rs")), ) .push( Router::new() .host("localhost") .path("{**rest}") .goal(Proxy::use_hyper_client("https://crates.io")), ); let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } ``` **Cargo.toml** ```toml file="/codes/proxy-simple/Cargo.toml" [package] name = "example-proxy-simple" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true, features = ["proxy"] } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` --- url: /guide/features/rate-limiter.md --- # Rate Limiting Middleware providing rate limiting functionality. ## Key Features - `RateIssuer` provides an abstraction for identifying visitor keys. `RemoteIpIssuer` is one of its implementations, which determines visitors based on their IP addresses. Keys are not necessarily strings; any type satisfying the `Hash + Eq + Send + Sync + 'static` constraints can serve as a key. - `RateGuard` offers an abstraction for rate limiting algorithms. Default implementations include fixed window (`FixedGuard`) and sliding window (`SlidingGuard`). - `RateStore` provides data storage operations. `MokaStore` is a built-in in-memory cache implementation based on `moka`. You can also define your own implementation. - `RateLimiter` is a struct implementing `Handler`, which includes a `skipper` field to specify requests that should bypass rate limiting. By default, `none_skipper` is used, meaning no requests are skipped. - `QuotaGetter` provides an abstraction for retrieving quotas. It fetches a quota object based on the visitor's `Key`, allowing user quotas and other configurations to be stored in databases and dynamically modified or retrieved. _**Example Code**_ ### Static Quota Example **main.rs** ```rust file="/codes/rate-limiter-static/src/main.rs" use salvo::prelude::*; use salvo::rate_limiter::{BasicQuota, FixedGuard, MokaStore, RateLimiter, RemoteIpIssuer}; #[handler] async fn hello() -> &'static str { "Hello World" } #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); let limiter = RateLimiter::new( FixedGuard::new(), MokaStore::new(), RemoteIpIssuer, BasicQuota::per_second(1), ); let router = Router::with_hoop(limiter).get(hello); let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } ``` **Cargo.toml** ```toml file="/codes/rate-limiter-static/Cargo.toml" [package] name = "example-rate-limiter-static" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true, features = ["rate-limiter"] } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` ### Dynamic Quota Example **main.rs** ```rust file="/codes/rate-limiter-dynamic/src/main.rs" use std::borrow::Borrow; use std::collections::HashMap; use std::hash::Hash; use std::sync::LazyLock; use salvo::Error; use salvo::prelude::*; use salvo::rate_limiter::{ CelledQuota, MokaStore, QuotaGetter, RateIssuer, RateLimiter, SlidingGuard, }; static USER_QUOTAS: LazyLock> = LazyLock::new(|| { let mut map = HashMap::new(); map.insert("user1".into(), CelledQuota::per_second(1, 1)); map.insert("user2".into(), CelledQuota::set_seconds(1, 1, 5)); map.insert("user3".into(), CelledQuota::set_seconds(1, 1, 10)); map }); struct UserIssuer; impl RateIssuer for UserIssuer { type Key = String; async fn issue(&self, req: &mut Request, _depot: &Depot) -> Option { req.query::("user") } } struct CustomQuotaGetter; impl QuotaGetter for CustomQuotaGetter { type Quota = CelledQuota; type Error = Error; async fn get(&self, key: &Q) -> Result where String: Borrow, Q: Hash + Eq + Sync, { USER_QUOTAS .get(key) .cloned() .ok_or_else(|| Error::other("user not found")) } } #[handler] async fn limited() -> &'static str { "Limited page" } #[handler] async fn home() -> Text<&'static str> { Text::Html(HOME_HTML) } #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); let limiter = RateLimiter::new( SlidingGuard::new(), MokaStore::new(), UserIssuer, CustomQuotaGetter, ); let router = Router::new() .get(home) .push(Router::with_path("limited").hoop(limiter).get(limited)); let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } static HOME_HTML: &str = r#" Rate Limiter Dynamic

Rate Limiter Dynamic

This example shows how to set limit for different users.

Limited page for user1: 1/second

Limited page for user2: 1/5seconds

Limited page for user3: 1/10seconds

"#; ``` **Cargo.toml** ```toml file="/codes/rate-limiter-dynamic/Cargo.toml" [package] name = "example-rate-limiter-dynamic" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true, features = ["rate-limiter"] } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` --- url: /guide/features/request-id.md --- # Request Chain ID The Request ID middleware is highly flexible. The ID generator (IdGenerator) is used to generate IDs, and you can define your own ID generator as long as it implements the `IdGenerator` trait. The default generator provided is `UlidGenerator`. Additionally, you can control whether to overwrite an existing `requestid`. You can also set `header_name` and other configurations. For details, please [refer to the documentation](https://docs.rs/salvo_extra/latest/salvo_extra/request_id/index.html). _**Example Code**_ **main.rs** ```rust file="/codes/request-id/src/main.rs" use salvo::prelude::*; #[handler] async fn hello(req: &mut Request) -> String { format!("Request id: {:?}", req.header::("x-request-id")) } #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; let router = Router::new().hoop(RequestId::new()).get(hello); Server::new(acceptor).serve(router).await; } ``` **Cargo.toml** ```toml file="/codes/request-id/Cargo.toml" [package] name = "example-request-id" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true, features = ["request-id"] } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` --- url: /guide/features/serve-static.md --- # Static Server Middleware that serves static files or embedded files. For detailed API, please [view the documentation](https://docs.rs/salvo_extra/latest/salvo_extra/request_id/index.html). ## Use Cases Static file servers have a wide range of applications in web development: - Serving static resources for websites, such as CSS, JavaScript, images, etc. - Hosting Single Page Applications (SPAs) - Distributing documents, media files, downloadable content, etc. - Serving as a complete hosting solution for simple websites ## Main Features - `StaticDir` provides support for serving static files from local directories. It can accept a list of multiple directories as parameters. For example: **main.rs** ```rust file="/codes/static-dir-list/src/main.rs" use salvo::prelude::*; use salvo::serve_static::StaticDir; #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); let router = Router::with_path("{*path}").get( StaticDir::new([ "static-dir-list/static/boy", "static-dir-list/static/girl", "static/boy", "static/girl", ]) .include_dot_files(false) .defaults("index.html") .auto_list(true), ); let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } ``` **Cargo.toml** ```toml file="/codes/static-dir-list/Cargo.toml" [package] name = "example-static-dir-list" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true, features = ["serve-static"] } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` If a file is not found in the first directory, it will be searched for in the second directory. `StaticDir` supports serving pre-compressed files when available. For example, if files like `index.html`, `index.html.gz`, `index.html.zst`, and `index.html.br` exist, then `index.html.gz`, `index.html.zst`, and `index.html.br` are considered pre-compressed versions of `index.html`. The appropriate compressed file will be served based on the request information. - Provides support for `rust-embed`. For example: ## Pros and Cons of rust-embed **Pros:** - Compiles static files into the binary, simplifying deployment. - No need to manage static files separately in production environments. - Potentially better performance (memory access vs. disk I/O). - Enhanced security, as files cannot be accidentally modified. **Cons:** - Increases the size of the application binary. - Updating static content requires recompiling and redeploying the entire application. - Not suitable for large static content that changes frequently. **main.rs** ```rust file="/codes/static-embed-files/src/main.rs" use rust_embed::RustEmbed; use salvo::prelude::*; use salvo::serve_static::static_embed; #[derive(RustEmbed)] #[folder = "static"] struct Assets; #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); let router = Router::with_path("{*path}").get(static_embed::().fallback("index.html")); let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } ``` **Cargo.toml** ```toml file="/codes/static-embed-files/Cargo.toml" [package] name = "example-static-embed-files" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] rust-embed.workspace = true salvo = { workspace = true, features = ["serve-static"] } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` You can set a default page to display using the `default` option. The `with_fallback` and `fallback` methods allow you to specify a fallback file to serve when a requested file is not found, which is useful for certain single-page website applications. --- url: /guide/features/session.md --- # Session Middleware providing support for `Session`. ## What is Session Session refers to user data stored on the server side, typically used to track user state and store temporary data. In web applications, Session addresses the stateless nature of the HTTP protocol, enabling the server to recognize multiple requests from the same user. ## Using Session in Salvo In Salvo, the Session middleware offers a simple API for reading and writing session data. The `SessionDepot` can be used to obtain a session instance, while methods such as `get`, `set`, and `remove` allow for managing session data. ## Common Use Cases - User authentication and login state management - Temporary storage of form data - Storage of user preference settings _**Example Code**_ **main.rs** ```rust file="/codes/session-login/src/main.rs" use salvo::prelude::*; use salvo::session::{CookieStore, Session, SessionDepotExt, SessionHandler}; #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); let session_handler = SessionHandler::builder( CookieStore::new(), b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab", ) .build() .unwrap(); let router = Router::new() .hoop(session_handler) .get(home) .push(Router::with_path("login").get(login).post(login)) .push(Router::with_path("logout").get(logout)); let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } #[handler] pub async fn login(req: &mut Request, depot: &mut Depot, res: &mut Response) { if req.method() == salvo::http::Method::POST { let mut session = Session::new(); session .insert("username", req.form::("username").await.unwrap()) .unwrap(); depot.set_session(session); res.render(Redirect::other("/")); } else { res.render(Text::Html(LOGIN_HTML)); } } #[handler] pub async fn logout(depot: &mut Depot, res: &mut Response) { if let Some(session) = depot.session_mut() { session.remove("username"); } res.render(Redirect::other("/")); } #[handler] pub async fn home(depot: &mut Depot, res: &mut Response) { let mut content = r#"Login"#.into(); if let Some(session) = depot.session_mut() && let Some(username) = session.get::("username") { content = format!(r#"Hello, {username}.
Logout"#); } res.render(Text::Html(content)); } static LOGIN_HTML: &str = r#" Login

Login

"#; ``` **Cargo.toml** ```toml file="/codes/session-login/Cargo.toml" [package] name = "example-session-login" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true, features = ["session"] } serde = { workspace = true, features = ["derive"] } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` --- url: /guide/features/size-limiter.md --- # File Upload Size Limiter Middleware A middleware for limiting the size of uploaded files in requests. ## Use Cases Size Limiter is primarily used in the following scenarios: - Preventing users from uploading excessively large files that consume server resources - Avoiding server memory overflow caused by malicious attackers uploading massive amounts of data - Setting different request size limit policies for different API endpoints - Improving server request processing efficiency and stability _**Example Code**_ **main.rs** ```rust file="/codes/size-limiter/src/main.rs" use std::fs::create_dir_all; use std::path::Path; use salvo::prelude::*; #[handler] async fn index(res: &mut Response) { res.render(Text::Html(INDEX_HTML)); } #[handler] async fn upload(req: &mut Request, res: &mut Response) { let file = req.file("file").await; if let Some(file) = file { let dest = format!("temp/{}", file.name().unwrap_or("file")); tracing::debug!(dest, "upload file"); if let Err(e) = std::fs::copy(file.path(), Path::new(&dest)) { res.status_code(StatusCode::INTERNAL_SERVER_ERROR); res.render(Text::Plain(format!("file not found in request: {e}"))); } else { res.render(Text::Plain(format!("File uploaded to {dest}"))); } } else { res.status_code(StatusCode::BAD_REQUEST); res.render(Text::Plain("file not found in request")); } } #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); create_dir_all("temp").unwrap(); let router = Router::new() .get(index) .push( Router::new() .hoop(max_size(1024 * 1024 * 10)) .path("limited") .post(upload), ) .push(Router::with_path("unlimit").post(upload)); let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } static INDEX_HTML: &str = r#" Upload file

Upload file

Unlimit

Limited 10MiB

"#; ``` **Cargo.toml** ```toml file="/codes/size-limiter/Cargo.toml" [package] name = "example-size-limiter" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true, features = ["size-limiter"] } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` --- url: /guide/features/sse.md --- # Server-Sent Events (SSE) ## What is SSE Server-Sent Events (SSE) is a web technology that allows servers to push data to clients. Unlike WebSocket, SSE is a one-way communication method, where data can only be sent from the server to the client. It is suitable for scenarios where the server needs to push data in real-time, but the client does not need to send data frequently. SSE is based on the HTTP protocol, easy to implement, and comes with built-in reconnection mechanisms. Provides middleware support for `SSE`. _**Example Code**_ **main.rs** ```rust file="/codes/sse/src/main.rs" // Copyright (c) 2018-2020 Sean McArthur // Licensed under the MIT license http://opensource.org/licenses/MIT //port from https://github.com/seanmonstar/warp/blob/master/examples/sse.rs use std::convert::Infallible; use std::time::Duration; use futures_util::StreamExt; use salvo::prelude::*; use salvo::sse::{self, SseEvent}; use tokio::time::interval; use tokio_stream::wrappers::IntervalStream; // create server-sent event fn sse_counter(counter: u64) -> Result { Ok(SseEvent::default().text(counter.to_string())) } #[handler] async fn handle_tick(res: &mut Response) { let event_stream = { let mut counter: u64 = 0; let interval = interval(Duration::from_secs(1)); let stream = IntervalStream::new(interval); stream.map(move |_| { counter += 1; sse_counter(counter) }) }; sse::stream(res, event_stream); } #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); let router = Router::with_path("ticks").get(handle_tick); let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } ``` **Cargo.toml** ```toml file="/codes/sse/Cargo.toml" [package] name = "example-sse" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] futures-util.workspace = true salvo = { workspace = true, features = ["sse"] } tokio = { workspace = true, features = ["macros"] } tokio-stream = { workspace = true, features = ["net"] } tracing.workspace = true tracing-subscriber.workspace = true ``` ## Chat Application Example **main.rs** ```rust file="/codes/sse-chat/src/main.rs" // Copyright (c) 2018-2020 Sean McArthur // Licensed under the MIT license http://opensource.org/licenses/MIT // // port from https://github.com/seanmonstar/warp/blob/master/examples/sse_chat.rs use std::collections::HashMap; use std::sync::LazyLock; use std::sync::atomic::{AtomicUsize, Ordering}; use futures_util::StreamExt; use parking_lot::Mutex; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; use salvo::prelude::*; use salvo::sse::{SseEvent, SseKeepAlive}; type Users = Mutex>>; static NEXT_USER_ID: AtomicUsize = AtomicUsize::new(1); static ONLINE_USERS: LazyLock = LazyLock::new(Users::default); #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); let router = Router::new().goal(index).push( Router::with_path("chat") .get(user_connected) .push(Router::with_path("{id}").post(chat_send)), ); let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } #[derive(Debug)] enum Message { UserId(usize), Reply(String), } #[handler] async fn chat_send(req: &mut Request, res: &mut Response) { let my_id = req.param::("id").unwrap(); let msg = std::str::from_utf8(req.payload().await.unwrap()).unwrap(); user_message(my_id, msg); res.status_code(StatusCode::OK); } #[handler] async fn user_connected(res: &mut Response) { // Use a counter to assign a new unique ID for this user. let my_id = NEXT_USER_ID.fetch_add(1, Ordering::Relaxed); tracing::info!("new chat user: {}", my_id); // Use an unbounded channel to handle buffering and flushing of messages // to the event source... let (tx, rx) = mpsc::unbounded_channel(); let rx = UnboundedReceiverStream::new(rx); tx.send(Message::UserId(my_id)) // rx is right above, so this cannot fail .unwrap(); // Save the sender in our list of connected users. ONLINE_USERS.lock().insert(my_id, tx); // Convert messages into Server-Sent Events and returns resulting stream. let stream = rx.map(|msg| match msg { Message::UserId(my_id) => { Ok::<_, salvo::Error>(SseEvent::default().name("user").text(my_id.to_string())) } Message::Reply(reply) => Ok(SseEvent::default().text(reply)), }); SseKeepAlive::new(stream).stream(res); } fn user_message(my_id: usize, msg: &str) { let new_msg = format!(": {msg}"); // New message from this user, send it to everyone else (except same uid)... // // We use `retain` instead of a for loop so that we can reap any user that // appears to have disconnected. ONLINE_USERS.lock().retain(|uid, tx| { if my_id == *uid { // don't send to same user, but do retain true } else { // If not `is_ok`, the SSE stream is gone, and so don't retain tx.send(Message::Reply(new_msg.clone())).is_ok() } }); } #[handler] async fn index(res: &mut Response) { res.render(Text::Html(INDEX_HTML)); } static INDEX_HTML: &str = r#" SSE Chat

SSE Chat

Connecting...

"#; ``` **Cargo.toml** ```toml file="/codes/sse-chat/Cargo.toml" [package] name = "example-sse-chat" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] futures-util.workspace = true parking_lot = "0.12" salvo = { workspace = true, features = ["sse"] } tokio = { workspace = true, features = ["macros"] } tokio-stream = { workspace = true, features = ["net"] } tracing.workspace = true tracing-subscriber.workspace = true ``` ## Integration with Large Language Models When integrating AI using OpenAI SDK or other large language model SDKs, streaming responses (Stream) are a common requirement. SSE provides a perfect solution, enabling real-time delivery of AI-generated content to the frontend, thereby offering a better user experience. For example, when using OpenAI's Chat Completion API with streaming responses, Salvo's SSE functionality can be leveraged to send AI-generated content incrementally to the client, eliminating the delay of waiting for a complete response. --- url: /guide/features/third-party.md --- # Third-Party WebSocket Plugin ## salvo-websocket ### Add Project Dependency ```toml [dependencies] salvo-websocket = "0.0.4" ``` ### Define Query Parameters for WebSocket Connection ```rust #[derive(Debug, Clone, Deserialize)] struct User { name: String, room: String, } ``` ### Implement the WebSocketHandler Trait ```rust impl WebSocketHandler for User { // Connection event async fn on_connected(&self, ws_id: usize, sender: UnboundedSender>) { tracing::info!("{} connected", ws_id); WS_CONTROLLER.write().await.join_group(self.room.clone(), sender).unwrap(); WS_CONTROLLER.write().await.send_group( self.room.clone(), Message::text(format!("{:?} joined!", self.name) ), ).unwrap(); } // Disconnection event async fn on_disconnected(&self, ws_id: usize) { tracing::info!("{} disconnected", ws_id); } // Message reception event async fn on_receive_message(&self, msg: Message) { tracing::info!("{:?} received", msg); let msg = if let Ok(s) = msg.to_str() { s } else { return; }; let new_msg = format!(": {}", self.name, msg); WS_CONTROLLER.write().await.send_group(self.room.clone(), Message::text(new_msg.clone())).unwrap(); } async fn on_send_message(&self, msg: Message) -> Result { tracing::info!("{:?} sending", msg); Ok(msg) } } ``` ### Write Connection Handling Method ```rust #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); let router = Router::new() .push(Router::with_path("chat").handle(user_connected)); tracing::info!("Listening on http://127.0.0.1:8698"); let acceptor = TcpListener::new("127.0.0.1:8698").bind().await; Server::new(acceptor).serve(router).await; } #[handler] async fn user_connected(req: &mut Request, res: &mut Response) -> Result<(), StatusError> { let user: Result = req.parse_queries(); match user { Ok(user) => { WebSocketUpgrade::new().upgrade(req, res, |ws| async move { handle_socket(ws, user).await; }).await } Err(_err) => { Err(StatusError::bad_request()) } } } ``` For more details, please refer directly to the [example](https://github.com/salvo-rs/salvo/tree/main/examples/ws-chat-with-salvo-websocket). --- url: /guide/features/timeout.md --- # Timeout Middleware Provides middleware support for timeout handling. ## Use Cases In web services, certain requests may take too long to process due to various reasons (such as slow database queries, delayed external service responses, etc.). To prevent these long-running requests from consuming server resources and affecting the processing of other requests, a timeout middleware can be used to set a maximum processing time limit for requests. If this time limit is exceeded, the request will be automatically terminated, and a timeout error will be returned. _**Example Code**_ **main.rs** ```rust file="/codes/timeout/src/main.rs" use std::time::Duration; use salvo::prelude::*; #[handler] async fn fast() -> &'static str { "hello" } #[handler] async fn slow() -> &'static str { tokio::time::sleep(Duration::from_secs(6)).await; "hello" } #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; let router = Router::new() .hoop(Timeout::new(Duration::from_secs(5))) .push(Router::with_path("slow").get(slow)) .push(Router::with_path("fast").get(fast)); Server::new(acceptor).serve(router).await; } ``` **Cargo.toml** ```toml file="/codes/timeout/Cargo.toml" [package] name = "example-timeout" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true, features = ["timeout"] } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` In the example above, we created two handler functions: a fast-responding `fast` and a `slow` that delays for 6 seconds before responding. A timeout limit of 5 seconds is set for all requests using `Timeout::new(Duration::from_secs(5))`. When accessing the `/slow` path, the request will time out because the processing time exceeds the 5-second limit. In contrast, when accessing the `/fast` path, the request will be processed normally and return a result. --- url: /guide/features/tower-compat.md --- # Tower Middleware Compatibility Salvo provides compatibility support for the Tower ecosystem through the `tower-compat` feature. For specific APIs, please [refer to the documentation](https://docs.rs/salvo_extra/latest/salvo_extra/tower_compat/index.html). ## Core Concepts of Tower Tower is a service abstraction library primarily based on two core traits: ### Service Trait ```rust pub trait Service { type Response; type Error; type Future: Future>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll>; fn call(&mut self, req: Request) -> Self::Future; } ``` The `Service` trait is responsible for handling requests and returning responses, similar to Salvo's handlers. ### Layer Trait ```rust pub trait Layer { type Service; fn layer(&self, inner: S) -> Self::Service; } ``` The `Layer` trait is used to wrap services and add additional functionality, similar to Salvo's middleware. ## Salvo Compatibility Support Salvo provides two key traits to achieve compatibility with the Tower ecosystem: - **TowerLayerCompat**: Converts Tower's `Layer` into Salvo's `Handler`, allowing it to be used as a Hoop (middleware). - **TowerServiceCompat**: Converts Tower's `Service` into Salvo's `Handler`. Usage Example: ```rust // Use Tower's RateLimitLayer and convert it to a Salvo-compatible format via the compat() method let limit = RateLimitLayer::new(5, Duration::from_secs(30)).compat(); let router = Router::new().hoop(limit).get(hello); ``` _**Example Code**_ **main.rs** ```rust file="/codes/with-tower/src/main.rs" use salvo::prelude::*; use tokio::time::Duration; use tower::limit::RateLimitLayer; #[handler] async fn hello() -> &'static str { "Hello World" } #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); let limit = RateLimitLayer::new(5, Duration::from_secs(30)).compat(); let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; let router = Router::new().hoop(limit).get(hello); Server::new(acceptor).serve(router).await; } ``` **Cargo.toml** ```toml file="/codes/with-tower/Cargo.toml" [package] name = "example-with-tower" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true, features = ["tower-compat"] } tokio = { workspace = true, features = ["macros"] } tower = { workspace = true , features = ["limit"] } tracing = { workspace = true } tracing-subscriber = { workspace = true } ``` --- url: /guide/features/trailing-slash.md --- # Trailing Slash A middleware for automatically adding or removing trailing `/`. ## Use Cases The Trailing Slash middleware is particularly useful in the following scenarios: - **URL Normalization**: Ensures all URLs follow a consistent format (by uniformly adding or removing trailing slashes), which helps improve SEO and avoid duplicate content issues. - **Simplified Route Handling**: Eliminates the need to write separate routing logic for URLs with and without trailing slashes, as the middleware automatically handles this conversion. - **Compatibility**: Some clients may automatically add or remove trailing slashes from URLs. This middleware ensures that requests are correctly routed. - **Redirect Management**: Can be configured to automatically redirect URLs with trailing slashes to those without (or vice versa), enhancing the user experience. - **Avoiding Route Conflicts**: In many web frameworks, `/path` and `/path/` may be treated as different routes. This middleware can handle them uniformly. _**Example Code**_ **main.rs** ```rust file="/codes/trailing-slash/src/main.rs" use salvo::prelude::*; use salvo::trailing_slash::add_slash; #[handler] async fn hello() -> &'static str { "Hello" } #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); let router = Router::with_hoop(add_slash()) .push(Router::with_path("hello").get(hello)) .push(Router::with_path("hello.world").get(hello)); let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } ``` **Cargo.toml** ```toml file="/codes/trailing-slash/Cargo.toml" [package] name = "example-trailing-slash" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true, features = ["trailing-slash"] } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` --- url: /guide/features/websocket.md --- # WebSocket A middleware that provides support for `WebSocket`. ## Introduction to WebSocket WebSocket is a protocol that enables full-duplex communication over a single TCP connection. It simplifies data exchange between clients and servers by allowing the server to actively push data to the client. In the WebSocket protocol, the browser and server only need to perform a handshake once to establish a persistent connection and enable bidirectional data transmission. Unlike traditional HTTP requests, a WebSocket connection remains open after being established, avoiding frequent connection setup and teardown, which significantly reduces communication overhead. This makes WebSocket particularly suitable for real-time applications, such as online games, chat applications, stock tickers, and other applications that require real-time updates. Salvo provides a concise and powerful API through its WebSocket middleware, enabling developers to easily integrate WebSocket functionality into their applications. _**Example Code**_ **main.rs** ```rust file="/codes/websocket/src/main.rs" use salvo::prelude::*; use salvo::websocket::WebSocketUpgrade; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, Serialize)] struct User { id: usize, name: String, } #[handler] async fn connect(req: &mut Request, res: &mut Response) -> Result<(), StatusError> { let user = req.parse_queries::(); WebSocketUpgrade::new() .upgrade(req, res, |mut ws| async move { println!("{user:#?} "); while let Some(msg) = ws.recv().await { let msg = if let Ok(msg) = msg { msg } else { // client disconnected return; }; if ws.send(msg).await.is_err() { // client disconnected return; } } }) .await } #[handler] async fn index(res: &mut Response) { res.render(Text::Html(INDEX_HTML)); } #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); let router = Router::new() .get(index) .push(Router::with_path("ws").goal(connect)); let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } static INDEX_HTML: &str = r#" WS

WS

Connecting...

"#; ``` **Cargo.toml** ```toml file="/codes/websocket/Cargo.toml" [package] name = "example-websocket" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] futures-util.workspace = true salvo = { workspace = true, features = ["websocket"] } tokio = { workspace = true, features = ["macros"] } tracing = { workspace = true } tracing-subscriber = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } ``` ### Chat Application Example **main.rs** ```rust file="/codes/websocket-chat/src/main.rs" // Copyright (c) 2018-2020 Sean McArthur // Licensed under the MIT license http://opensource.org/licenses/MIT // // port from https://github.com/seanmonstar/warp/blob/master/examples/websocket_chat.rs use std::collections::HashMap; use std::sync::LazyLock; use std::sync::atomic::{AtomicUsize, Ordering}; use futures_util::{FutureExt, StreamExt}; use tokio::sync::{RwLock, mpsc}; use tokio_stream::wrappers::UnboundedReceiverStream; use salvo::prelude::*; use salvo::websocket::{Message, WebSocket, WebSocketUpgrade}; type Users = RwLock>>>; static NEXT_USER_ID: AtomicUsize = AtomicUsize::new(1); static ONLINE_USERS: LazyLock = LazyLock::new(Users::default); #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); let router = Router::new() .goal(index) .push(Router::with_path("chat").goal(user_connected)); let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } #[handler] async fn user_connected(req: &mut Request, res: &mut Response) -> Result<(), StatusError> { WebSocketUpgrade::new() .upgrade(req, res, handle_socket) .await } async fn handle_socket(ws: WebSocket) { // Use a counter to assign a new unique ID for this user. let my_id = NEXT_USER_ID.fetch_add(1, Ordering::Relaxed); tracing::info!("new chat user: {}", my_id); // Split the socket into a sender and receive of messages. let (user_ws_tx, mut user_ws_rx) = ws.split(); // Use an unbounded channel to handle buffering and flushing of messages // to the websocket... let (tx, rx) = mpsc::unbounded_channel(); let rx = UnboundedReceiverStream::new(rx); let fut = rx.forward(user_ws_tx).map(|result| { if let Err(e) = result { tracing::error!(error = ?e, "websocket send error"); } }); tokio::task::spawn(fut); let fut = async move { ONLINE_USERS.write().await.insert(my_id, tx); while let Some(result) = user_ws_rx.next().await { let msg = match result { Ok(msg) => msg, Err(e) => { eprintln!("websocket error(uid={my_id}): {e}"); break; } }; user_message(my_id, msg).await; } user_disconnected(my_id).await; }; tokio::task::spawn(fut); } async fn user_message(my_id: usize, msg: Message) { let Ok(msg) = msg.as_str() else { return; }; let new_msg = format!(": {msg}"); // New message from this user, send it to everyone else (except same uid)... for (&uid, tx) in ONLINE_USERS.read().await.iter() { if my_id != uid && let Err(_disconnected) = tx.send(Ok(Message::text(new_msg.clone()))) { // The tx is disconnected, our `user_disconnected` code // should be happening in another task, nothing more to // do here. tracing::info!("the tx is disconnected, uid={uid}"); } } } async fn user_disconnected(my_id: usize) { eprintln!("good bye user: {my_id}"); // Stream closed up, so remove from the user list ONLINE_USERS.write().await.remove(&my_id); } #[handler] async fn index(res: &mut Response) { res.render(Text::Html(INDEX_HTML)); } static INDEX_HTML: &str = r#" WS Chat

WS Chat

Connecting...

"#; ``` **Cargo.toml** ```toml file="/codes/websocket-chat/Cargo.toml" [package] name = "example-websocket-chat" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] futures-util.workspace = true salvo = { workspace = true, features = ["websocket"] } tokio = { workspace = true, features = ["macros"] } tokio-stream = { workspace = true, features = ["net"] } tracing.workspace = true tracing-subscriber.workspace = true ``` --- url: /guide/features/webtransport.md --- # WebTransport WebTransport is a network transport protocol based on HTTP/3, providing bidirectional communication capabilities between clients and servers while ensuring low latency, high throughput, and security. ## Use Cases WebTransport is particularly suitable for the following scenarios: - **Real-time Applications**: Online gaming, real-time collaboration tools, video conferencing, and other applications requiring low-latency communication. - **Large File Transfers**: Supports high-throughput data transmission, ideal for media streaming and large file uploads/downloads. - **Multiplexed Communication**: Allows simultaneous establishment of multiple bidirectional and unidirectional data streams. - **Datagram Communication**: Supports datagram communication without guaranteed order or reliability, suitable for scenarios with extremely high real-time requirements. Compared to WebSocket, WebTransport offers lower latency and more flexible communication patterns, especially performing better in unstable network environments. ## Salvo Implementation The Salvo framework provides built-in support for WebTransport, allowing developers to easily build WebTransport-based applications. Key features include: - Support for establishing WebTransport sessions - Support for bidirectional stream communication - Support for unidirectional stream communication - Support for datagram transmission - Server-initiated communication streams ### Simple Example Below is a simplified example of implementing a WebTransport server using Salvo: ```rust #[handler] async fn connect(req: &mut Request) -> Result<(), salvo::Error> { let session = req.web_transport_mut().await.unwrap(); // Handle datagrams if let Ok(Some((_, datagram))) = session.accept_datagram().await { // Process received datagram let mut resp = BytesMut::from(&b"Response: "[..]); resp.put(datagram); session.send_datagram(resp.freeze())?; } // Handle bidirectional streams if let Ok(Some(webtransport::server::AcceptedBi::BidiStream(_, stream))) = session.accept_bi().await { let (send, recv) = salvo::proto::quic::BidiStream::split(stream); // Process bidirectional stream data } Ok(()) } ``` ### Configuration and Startup Starting a Salvo application with WebTransport support requires configuring TLS certificates and a QUIC listener: ```rust let cert = include_bytes!("../certs/cert.pem").to_vec(); let key = include_bytes!("../certs/key.pem").to_vec(); // Configure routing let router = Router::new().push(Router::with_path("counter").goal(connect)); // Configure TLS let config = RustlsConfig::new(Keycert::new().cert(cert.as_slice()).key(key.as_slice())); // Set up listeners let listener = TcpListener::new(("0.0.0.0", 8698)).rustls(config.clone()); let acceptor = QuinnListener::new(config, ("0.0.0.0", 8698)) .join(listener) .bind() .await; // Start server Server::new(acceptor).serve(router).await; ``` ## Complete Example To learn more about using WebTransport in Salvo, check out the complete example on GitHub: [https://github.com/salvo-rs/salvo/blob/main/examples/webtransport](https://github.com/salvo-rs/salvo/blob/main/examples/webtransport) This example includes complete implementations for both server and client, demonstrating how to handle various types of WebTransport communication. --- url: /guide/index.md --- # To Master This Art ## Why Build This Framework As a beginner, I found myself struggling to grasp existing frameworks like actix-web and Rocket. When I attempted to rewrite a previous Go web service in Rust, each framework seemed more complex than those available in Go. Given Rust's already steep learning curve, why complicate web frameworks further? When Tokio introduced the Axum framework, I was thrilled, thinking I could finally stop maintaining my own web framework. However, I soon realized that while Axum appeared simple, it required extensive type gymnastics and generic definitions. Crafting even a basic middleware demanded deep Rust expertise and writing copious amounts of obscure boilerplate code. Thus, I decided to continue maintaining my unique web framework—one that is intuitive, feature-rich, and beginner-friendly. ## Is Salvo Right for You? Salvo is simple yet comprehensive and powerful, arguably the strongest in the Rust ecosystem. Despite its capabilities, it remains easy to learn and use, sparing you any "self-castration" pains. - **Ideal for Rust beginners**: CRUD operations are commonplace, and with Salvo, such tasks feel as straightforward as with frameworks in other languages (e.g., Express, Koa, Gin, Flask). In some aspects, Salvo is even more abstract and concise. - **Suitable for production**: If you aim to deploy Rust in production for robust, high-speed servers, Salvo fits the bill. Although not yet at version 1.0, its core features have undergone years of iteration, ensuring stability and timely issue resolution. - **Perfect for you**, especially if your hair is thinning and shedding daily. ## Achieving Simplicity Hyper handles many low-level implementations, making it a reliable foundation for general needs. Salvo follows this approach, offering a powerful and flexible routing system alongside essential features like Acme, OpenAPI, and JWT authentication. Salvo unifies Handlers and Middleware: Middleware _is_ a Handler. Both are attached to the Router via `hoop`. Essentially, they process `Request` and may write data to `Response`. A Handler receives three parameters: `Request`, `Depot` (for temporary data during request processing), and `Response`. For convenience, you can omit unnecessary parameters or ignore their order. ```rust use salvo::prelude::*; #[handler] async fn hello_world(_req: &mut Request, _depot: &mut Depot, res: &mut Response) { res.render("Hello world"); } #[handler] async fn hello_world(res: &mut Response) { res.render("Hello world"); } ``` The routing API is exceptionally simple yet powerful. For typical use cases, you only need to focus on the `Router` type. Additionally, if a struct implements relevant traits, Salvo can automatically generate OpenAPI documentation, extract parameters, handle errors gracefully, and return user-friendly messages. This makes writing handlers as intuitive as writing ordinary functions. We'll explore these features in detail later. Here’s an example: ```rust #[endpoint(tags("message_logs"))] pub async fn create_message_log_handler( input: JsonBody, depot: &mut Depot, ) -> APPResult> { let db = utils::get_db(depot)?; let log = create_message_log(&input, db).await?; Ok(Json(log)) } ``` In this example, `JsonBody` automatically parses JSON from the request body into the `CreateOrUpdateMessageLog` type (supporting multiple data sources and nested types). The `#[endpoint]` macro generates OpenAPI documentation for this endpoint, simplifying parameter extraction and error handling. ## Routing System I believe Salvo’s routing system stands out. Routers can be flat or tree-like, distinguishing between _business logic trees_ and _access directory trees_. The business logic tree organizes routers based on business needs, which may not align with the access directory tree. Typically, routes are written like this: ```rust Router::new().path("articles").get(list_articles).post(create_article); Router::new() .path("articles/{id}") .get(show_article) .patch(edit_article) .delete(delete_article); ``` Often, viewing articles and listing them don’t require user login, but creating, editing, or deleting articles does. Salvo’s nested routing system elegantly handles this. We can group public routes together: ```rust Router::new() .path("articles") .get(list_articles) .push(Router::new().path("{id}").get(show_article)); ``` Then, group protected routes with middleware for authentication: ```rust Router::new() .path("articles") .hoop(auth_check) .post(create_article) .push(Router::new().path("{id}").patch(edit_article).delete(delete_article)); ``` Even though both routers share the same `path("articles")`, they can be added to the same parent router, resulting in: ```rust Router::new() .push( Router::new() .path("articles") .get(list_articles) .push(Router::new().path("{id}").get(show_article)), ) .push( Router::new() .path("articles") .hoop(auth_check) .post(create_article) .push(Router::new().path("{id}").patch(edit_article).delete(delete_article)), ); ``` `{id}` matches a path segment. Typically, an article `id` is numeric, so we can restrict matching with a regex: `r"{id:/\d+/}"`. --- url: /guide/quick-start.md --- # Quick Start ## Install Rust If you haven't installed Rust yet, you can use the official (`rustup`)\[[https://doc.rust-lang.org/book/ch01-01-installation.html](https://doc.rust-lang.org/book/ch01-01-installation.html)] to install Rust. :::tip The minimum supported Rust version for Salvo is 1.94. Run `rustup update` to ensure you have a compatible Rust version installed. ::: ## Write Your First Salvo Program Create a new project: ```bash cargo new hello --bin ``` :::details How to install Salvo ```bash cargo add salvo ``` ::: Add dependencies to `Cargo.toml`: ```toml file="/codes/hello/Cargo.toml" [package] name = "example-hello" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` In `main.rs`, create a simple function handler named `hello`, which simply prints the text `"Hello world"`. ```rust file="/codes/hello/src/main.rs" use salvo::prelude::*; // Handler for English greeting #[handler] async fn hello() -> &'static str { "Hello World" } // Handler for Chinese greeting #[handler] async fn hello_zh() -> Result<&'static str, ()> { Ok("你好,世界!") } #[tokio::main] async fn main() { // Initialize logging subsystem tracing_subscriber::fmt().init(); // Bind server to port 8698 let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; // Create router with two endpoints: // - / (root path) returns English greeting // - /你好 returns Chinese greeting let router = Router::new() .get(hello) .push(Router::with_path("你好").get(hello_zh)); // Print router structure for debugging println!("{router:?}"); // Start serving requests Server::new(acceptor).serve(router).await; } ``` Congratulations! Your first Salvo program is complete. Just run `cargo run` in the command line, then open `http://127.0.0.1:8698` in your browser. ## Detailed Explanation Here, `hello_world` is a `Handler` used to process user requests. The `#[handler]` attribute allows a function to conveniently implement the `Handler` trait. Moreover, it enables us to write function parameters in various simplified ways. - Original form: ```rust #[handler] async fn hello(_req: &mut Request, _depot: &mut Depot, res: &mut Response, _ctrl: &mut FlowCtrl) { res.render("Hello world"); } ``` - You can omit unused parameters in the function. For example, `_req`, `_depot`, and `_ctrl` are not used here and can be omitted entirely: ```rust #[handler] async fn hello(res: &mut Response) { res.render("Hello world"); } ``` - Any type can be used as the function's return type as long as it implements the `Writer` trait. For instance, `&str` implements `Writer`, and when returned, it prints plain text: ```rust #[handler] async fn hello(res: &mut Response) -> &'static str { "Hello world" } ``` - More commonly, we need to use `Result` as the return type to handle errors during function execution. If both `T` and `E` implement `Writer`, then `Result` can be used as a return value: ```rust #[handler] async fn hello(res: &mut Response) -> Result<&'static str, ()> { Ok("Hello world") } ``` ## Sleek HTTP3 They say HTTP3 is as agile as a swallow, a dream many programmers have longed for but couldn't achieve. This time, Salvo makes it happen, allowing everyone to effortlessly enjoy the wonderful services brought by HTTP3! First, enable the HTTP3 feature in `Cargo.toml`, then modify `main.rs` as follows: **main.rs** ```rust file="/codes/hello-h3/src/main.rs" use salvo::conn::rustls::{Keycert, RustlsConfig}; use salvo::prelude::*; // Handler function responding with "Hello World" for HTTP/3 requests #[handler] async fn hello() -> &'static str { "Hello World" } #[tokio::main] async fn main() { // Initialize logging system tracing_subscriber::fmt().init(); // Load TLS certificate and private key from embedded PEM files let cert = include_bytes!("../certs/cert.pem").to_vec(); let key = include_bytes!("../certs/key.pem").to_vec(); // Create router with single endpoint let router = Router::new().get(hello); // Configure TLS settings using Rustls let config = RustlsConfig::new(Keycert::new().cert(cert.as_slice()).key(key.as_slice())); // Create TCP listener with TLS encryption on port 8698 let listener = TcpListener::new(("0.0.0.0", 8698)).rustls(config.clone()); // Create QUIC listener and combine with TCP listener let acceptor = QuinnListener::new(config.build_quinn_config().unwrap(), ("0.0.0.0", 8698)) .join(listener) .bind() .await; // Start server supporting both HTTP/3 (QUIC) and HTTPS (TCP) Server::new(acceptor).serve(router).await; } ``` **Cargo.toml** ```toml file="/codes/hello-h3/Cargo.toml" [package] name = "example-hello-h3" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true, features = ["quinn"] } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` ## Salvo CLI Tool 🛠️ [Salvo CLI](https://github.com/salvo-rs/salvo-cli) is a tool designed for the Salvo web framework. It helps create clean, readable code, saving your time for more interesting things in life. If you have ideas for improving the CLI or notice issues that need fixing, don't hesitate! Submit an issue—we welcome your insights. ### Step 1 Install the CLI tool: ```bash cargo install salvo-cli ``` ### Step 2 > Create a new Salvo project using the `new` command followed by your project name: > > ```bash > salvo new project_name > ``` With this simple CLI tool, you can quickly set up a Salvo project, allowing you to focus on implementing your business logic rather than project structure setup. ✨ ## More Examples It's recommended to clone the Salvo repository directly and run the examples in the `examples` directory. For instance, the following command runs the `hello` example: ```sh git clone https://github.com/salvo-rs/salvo cd salvo/examples cargo run --bin example-hello ``` There are many examples in the `examples` directory. You can run them using commands like `cargo run --bin example-`. --- url: /guide/topics/ai-skills.md --- # AI Skills [Salvo Skills](https://github.com/salvo-rs/salvo-skills) is a collection of 27 specialized AI agent skills designed for the Salvo web framework. These skills follow the [Agent Skills](https://agent-skills.github.io/) open standard and help AI assistants understand and generate Salvo code more effectively. ## What Are AI Skills? AI Skills are structured knowledge modules that provide AI coding assistants with deep, framework-specific expertise. When integrated into your development environment, they enable AI tools to: - Generate accurate, idiomatic Salvo code - Understand Salvo's architecture and conventions (e.g., `#[handler]` macro, Router, Depot) - Provide contextual guidance on topics like routing, middleware, authentication, WebSocket, and more ## Available Skills (27 Total) | Category | Skills | | --------------------- | ------------------------------------------------------------------------------------------------------------- | | **Core Framework** | `salvo-basic-app`, `salvo-routing`, `salvo-middleware`, `salvo-error-handling` | | **Data Handling** | `salvo-data-extraction`, `salvo-database`, `salvo-file-handling`, `salvo-static-files`, `salvo-caching` | | **Security** | `salvo-auth`, `salvo-session`, `salvo-csrf`, `salvo-cors`, `salvo-rate-limiter`, `salvo-tls-acme` | | **Real-time** | `salvo-realtime`, `salvo-websocket`, `salvo-sse` | | **Performance & Ops** | `salvo-compression`, `salvo-timeout`, `salvo-concurrency-limiter`, `salvo-graceful-shutdown`, `salvo-logging` | | **Advanced** | `salvo-openapi`, `salvo-proxy`, `salvo-flash`, `salvo-testing` | ## Setup Guide ### Claude Code Copy the skills into your project's `.claude/skills/` directory: ```bash git clone https://github.com/salvo-rs/salvo-skills.git cp -r salvo-skills/skills .claude/skills/ ``` Skills load automatically when Claude Code works with your Salvo project. ### GitHub Copilot (VS Code) 1. Copy the skills into `.github/skills/`: ```bash git clone https://github.com/salvo-rs/salvo-skills.git cp -r salvo-skills/skills .github/skills/ ``` 2. Enable agent skills in VS Code settings: ```json { "chat.useAgentSkills": true } ``` Skills activate automatically when you discuss Salvo topics with Copilot. ### Cursor Add skills as documentation context in Cursor: 1. Clone the repository: ```bash git clone https://github.com/salvo-rs/salvo-skills.git ``` 2. Create or edit the `.cursor/rules` file in your project root and add: ``` Read and follow the skill files in .cursor/skills/ directory for Salvo framework guidance. ``` 3. Copy the skills: ```bash cp -r salvo-skills/skills .cursor/skills/ ``` Cursor will reference these skill files when generating Salvo-related code. ### Windsurf Configure skills as knowledge context in Windsurf: 1. Clone the repository: ```bash git clone https://github.com/salvo-rs/salvo-skills.git ``` 2. Create or edit the `.windsurfrules` file in your project root and add: ``` Read and follow the skill files in .windsurf/skills/ directory for Salvo framework guidance. ``` 3. Copy the skills: ```bash cp -r salvo-skills/skills .windsurf/skills/ ``` ### Other AI Tools For any AI coding assistant that supports custom instructions or context files, you can: 1. Clone the repository: `git clone https://github.com/salvo-rs/salvo-skills.git` 2. Place the `skills/` folder in your project directory 3. Configure your AI tool to reference these files as context ## Links - **GitHub Repository**: [https://github.com/salvo-rs/salvo-skills](https://github.com/salvo-rs/salvo-skills) - **Agent Skills Standard**: [https://agent-skills.github.io/](https://agent-skills.github.io/) --- url: /guide/topics/deployment.md --- # How to Deploy Applications A Salvo project, after compilation, becomes an executable file. For deployment, you only need to upload this executable along with its dependent static resources to the server. For Rust-based projects, there is also a very simple deployment platform: [shuttle.rs](https://www.shuttle.rs). Shuttle provides support for Salvo-like projects. For details, please refer to the [official documentation](https://docs.shuttle.rs/guide/salvo-examples.html). ## Docker Deployment You can also use Docker to deploy Salvo applications. Below is a basic `Dockerfile` example, which you can adjust according to your project's requirements: ```dockerfile # Build stage FROM rust:slim AS build WORKDIR /app # Copy dependency files first to build dependencies (leveraging cache layers) COPY Cargo.toml Cargo.lock ./ RUN mkdir src && \ echo 'fn main() { println!("Placeholder"); }' > src/main.rs && \ cargo build --release # Copy actual source code and build the application COPY src ./src/ RUN touch src/main.rs && \ cargo build --release # Use a lightweight base image for the runtime stage FROM debian:bookworm-slim RUN apt-get update && \ apt-get install -y --no-install-recommends ca-certificates && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* # Create a non-root user to run the application RUN useradd -ms /bin/bash appuser USER appuser WORKDIR /app # Copy the binary file from the build stage COPY --from=build /app/target/release/your_app_name ./app # Set the container startup command CMD ["./app"] ``` ### Usage Instructions 1. Save the above `Dockerfile` in your project's root directory. 2. Adjust `your_app_name` to the actual binary file name according to your project. 3. If your application requires static files (such as templates, CSS, JS, etc.), you can add corresponding `COPY` commands. 4. If your application requires environment variable configuration, you can use the `ENV` instruction. 5. Build the Docker image: ```bash docker build -t your-app-name . ``` 6. Run the container: ```bash docker run -p 8698:8698 your-app-name ``` Please adjust the port number and other configurations according to your application's actual needs. --- url: /guide/topics/graceful-shutdown.md --- # Graceful Shutdown Graceful shutdown refers to the process where, when a server is being shut down, it does not immediately terminate all connections. Instead, it first stops accepting new requests while allowing existing requests sufficient time to complete their processing before closing the service. This approach prevents requests from being abruptly interrupted, thereby improving user experience and system reliability. Salvo provides support for graceful shutdown through the `handle` method of the `Server`, which retrieves the server handle, followed by calling the `stop_graceful` method to implement the shutdown. After invoking this method, the server will: - Stop accepting new connection requests - Wait for existing requests to complete processing - Forcefully close any remaining connections after a specified timeout (if provided) Here is a simple example: **main.rs** ```rust file="/codes/graceful-shutdown/src/main.rs" use salvo::prelude::*; use salvo::server::ServerHandle; use tokio::signal; #[tokio::main] async fn main() { // Bind server to port 8698 let acceptor = TcpListener::new("127.0.0.1:8698").bind().await; // Create server instance let server = Server::new(acceptor); // Get server handle for graceful shutdown let handle = server.handle(); // Listen Shutdown Signal tokio::spawn(listen_shutdown_signal(handle)); // Start serving requests (empty router in this example) server.serve(Router::new()).await; } async fn listen_shutdown_signal(handle: ServerHandle) { // Wait Shutdown Signal let ctrl_c = async { // Handle Ctrl+C signal signal::ctrl_c() .await .expect("failed to install Ctrl+C handler"); }; #[cfg(unix)] let terminate = async { // Handle SIGTERM on Unix systems signal::unix::signal(signal::unix::SignalKind::terminate()) .expect("failed to install signal handler") .recv() .await; }; #[cfg(windows)] let terminate = async { // Handle Ctrl+C on Windows (alternative implementation) signal::windows::ctrl_c() .expect("failed to install signal handler") .recv() .await; }; // Wait for either signal to be received tokio::select! { _ = ctrl_c => println!("ctrl_c signal received"), _ = terminate => println!("terminate signal received"), }; // Graceful Shutdown Server handle.stop_graceful(None); } ``` **Cargo.toml** ```toml file="/codes/graceful-shutdown/Cargo.toml" [package] name = "example-graceful-shutdown" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true } tokio = { workspace = true, features = ["macros", "signal"] } tracing.workspace = true tracing-subscriber.workspace = true ``` In the example above: - `server.handle()` retrieves the server handle, which can be used to control the server's lifecycle - `handle.stop_graceful(None)` initiates the graceful shutdown process, where `None` indicates no timeout is set, meaning the server will wait indefinitely for all requests to complete - To set a timeout, you can pass `Some(Duration)`, after which any remaining connections will be forcefully closed This approach is particularly suitable for applications deployed in container environments or on cloud platforms, as well as for scenarios requiring hot updates to ensure that requests are not unexpectedly interrupted. --- url: /guide/topics/handle-error.md --- # Error Handling ## Common Error Handling Approaches in Rust Applications Error handling in Rust differs from languages like Java; it lacks constructs like `try...catch`. The typical approach is to define a global error handling type at the application level: ```rust use thiserror::Error; #[derive(Error, Debug)] pub enum AppError { #[error("io: `{0}`")] Io(#[from] io::Error), #[error("utf8: `{0}`")] FromUtf8(#[from] FromUtf8Error), #[error("diesel: `{0}`")] Diesel(#[from] diesel::result::Error), ... } pub type AppResult = Result; ``` Here, the `thiserror` library is used, which facilitates defining custom error types and simplifies code. For brevity, an `AppResult` type alias is also defined. ### thiserror vs. anyhow In the Rust error handling ecosystem, two commonly used libraries are `thiserror` and `anyhow`: - **thiserror**: Suitable for library developers to define clear error types. It uses derive macros to help implement the `std::error::Error` trait for custom error types while allowing you to define error representations. When building a library or providing clear error types to users, `thiserror` is the better choice. - **anyhow**: Geared toward application developers, it provides a generic error type `anyhow::Error` that can encapsulate any error implementing the `std::error::Error` trait. It focuses more on error propagation than definition, making it particularly suitable for application-layer code. You can quickly convert various errors into `anyhow::Error`, reducing the need for boilerplate code. In some scenarios, you might use both libraries: define error types with `thiserror` in libraries and handle and propagate these errors with `anyhow` in applications. ## Error Handling in Handlers In Salvo, `Handler`s often encounter various errors, such as database connection errors, file access errors, network connection errors, etc. For these types of errors, the aforementioned error handling approach can be applied: ```rust #[handler] async fn home() -> AppResult<()> { } ``` Here, `home` directly returns an `AppResult<()>`. But how should this error be displayed? We need to implement the `Writer` trait for the custom error type `AppResult`, where we can decide how to display the error: ```rust #[async_trait] impl Writer for AppError { async fn write(mut self, _req: &mut Request, depot: &mut Depot, res: &mut Response) { res.render(Text::Plain("I'm a error, hahaha!")); } } ``` In Salvo, a `Handler` can return a `Result`, provided that both the `Ok` and `Err` types in the `Result` implement the `Writer` trait. ### Error Handling with anyhow Given the widespread use of anyhow, Salvo provides built-in support for `anyhow::Error`. When the `anyhow` feature is enabled, `anyhow::Error` implements the `Writer` trait and is mapped to `InternalServerError`: ```rust #[cfg(feature = "anyhow")] #[async_trait] impl Writer for ::anyhow::Error { async fn write(mut self, _req: &mut Request, _depot: &mut Depot, res: &mut Response) { res.render(StatusError::internal_server_error()); } } ``` To use the anyhow feature, enable Salvo's `anyhow` feature in Cargo.toml: ```toml [dependencies] salvo = { version = "0.95.0", features = ["anyhow"] } anyhow = "1.0" ``` This allows your handler functions to directly return `anyhow::Result`: ```rust #[handler] async fn home() -> anyhow::Result { let data = fetch_data().context("Failed to fetch data")?; Ok(Text::Plain(data)) } ``` Errors often contain sensitive information, which generally shouldn't be visible to regular users for security and privacy reasons. However, if you're a developer or site administrator, you might prefer errors to be fully exposed, revealing the most accurate error information. As shown, in the `write` method, we can access references to `Request` and `Depot`, making it convenient to implement the above approach: ```rust #[async_trait] impl Writer for AppError { async fn write(mut self, _req: &mut Request, depot: &mut Depot, res: &mut Response) { let user = depot.get_typed::(); if user.is_admin { res.render(Text::Plain(e.to_string())); } else { res.render(Text::Plain("I'm a error, hahaha!")); } } } ``` ## Displaying Error Pages Salvo's built-in error pages meet requirements in most cases, displaying Html, Json, or Xml pages based on the request's data type. However, there are situations where custom error page displays are still desired. This can be achieved by implementing a custom `Catcher`. For detailed instructions, refer to the [`Catcher`](/guide/concepts/catcher.md) section. --- url: /guide/topics/index.md --- # Topics ## Topics ### [Processing Flow](/guide/topics/processing-flow.md) - [Routing Matching Phase](/guide/topics/processing-flow.md#routing-matching-phase) - [Handler Execution Phase](/guide/topics/processing-flow.md#handler-execution-phase) - [Error-Catching Phase](/guide/topics/processing-flow.md#error-catching-phase) - [Salvo Request Lifecycle](/guide/topics/processing-flow.md#salvo-request-lifecycle) ### [Error Handling](/guide/topics/handle-error.md) - [Common Error Handling Approaches in Rust Applications](/guide/topics/handle-error.md#common-error-handling-approaches-in-rust-applications) - [Error Handling in Handlers](/guide/topics/handle-error.md#error-handling-in-handlers) - [Displaying Error Pages](/guide/topics/handle-error.md#displaying-error-pages) ### [Sending Files](/guide/topics/send-file.md) ### [Using Databases](/guide/topics/working-with-database.md) - [SeaORM](/guide/topics/working-with-database.md#seaorm) - [Tokio ORM (Toasty)](/guide/topics/working-with-database.md#tokio-orm-toasty) - [SurrealDB Rust SDK](/guide/topics/working-with-database.md#surrealdb-rust-sdk) ### [Using Template Engines](/guide/topics/use-template-engine.md) ### [Graceful Shutdown](/guide/topics/graceful-shutdown.md) ### [Writing Tests](/guide/topics/testing.md) - [The Importance of Testing](/guide/topics/testing.md#the-importance-of-testing) - [Salvo Testing Tools](/guide/topics/testing.md#salvo-testing-tools) ### [How to Deploy Applications](/guide/topics/deployment.md) - [Docker Deployment](/guide/topics/deployment.md#docker-deployment) ### [AI Skills](/guide/topics/ai-skills.md) - [What Are AI Skills?](/guide/topics/ai-skills.md#what-are-ai-skills) - [Available Skills (27 Total)](/guide/topics/ai-skills.md#available-skills-27-total) - [Setup Guide](/guide/topics/ai-skills.md#setup-guide) - [Links](/guide/topics/ai-skills.md#links) --- url: /guide/topics/processing-flow.md --- # Processing Flow `Service` first converts the request into Salvo's `Response`, then enters the routing matching phase. ## Routing Matching Phase Routing matching runs filters in the order they were added, proceeding from outer to inner and top to bottom. If any filter fails, the match is considered unsuccessful. During the matching process, the request's path information is consumed progressively. Once a path filter matches successfully, it consumes the matched portion of the path. When all path segments are consumed, and no filter fails along the matching chain, and the last `Router` on the current chain has a `goal` `Handler`, the match is successful, and the matching phase ends. All `Handler`s collected from the matching chain proceed to the execution phase. If the path is not fully consumed, no errors occur along the chain, but there are no further child routes to continue matching, the current chain is considered a match failure, and the process moves to the next route for matching. If all routes are matched without success, the process enters the error-catching phase. ## Handler Execution Phase The `Handler`s collected during the matching phase are executed sequentially. During execution, preceding middleware can call `ctrl::call_next()` to allow subsequent middleware to execute first before running its own logic. If an error status code or a redirect occurs during execution, subsequent `Handler`s will not be executed. If the status code indicates an error and the `Response`'s `Body` is unset or is `ResBody::Error`, the process enters the error-catching phase; otherwise, it skips the catching phase. ## Error-Catching Phase `Catcher` is a type used to handle errors and can also include middleware (hoops). Errors pass through all `Handler`s within the `Catcher` sequentially. If a `Handler` has already handled the error and does not want subsequent `Handler`s to continue, it can skip the rest using `ctrl.skip_rest()`, directly ending the catching phase. `Catcher` must include at least one `Handler` as the default error handler. The default is `DefaultGoal`, but you can fully customize your own `Handler` as the default error-handling implementation. It displays error information in the format requested by the `content-type` header, supporting `json`, `xml`, `text`, and `html` formats. `DefaultGoal` also provides display settings; for example, by default, it displays Salvo-related links when showing HTML format. You can call `DefaultGoal::footer` or `DefaultGoal::with_footer` to set a custom footer as desired. `Service` converts Salvo's `Response` into Hyper's `Response` type, which is ultimately returned to clients such as browsers. ## Salvo Request Lifecycle This is a visual representation and explanation of the HTTP request lifecycle in the Salvo web framework. ```mermaid flowchart TD subgraph MainGraph[Salvo Framework Request Lifecycle] Start[Client Request] --> Convert[Service converts HTTP request to Salvo Response] %% Routing Matching Phase subgraph RoutingPhase[1. Routing Matching Phase] Convert --> Routing[Match routes in added order, outer to inner, top to bottom] Routing --> FilterCheck{Do all filters pass?} FilterCheck -->|No| NextRoute[Try next route] FilterCheck -->|Yes| PathConsume[Consume matched path segment] PathConsume --> PathCheck{Is path fully consumed with a goal Handler?} PathCheck -->|No| SubRouteCheck{Are there sub-routes to continue matching?} SubRouteCheck -->|Yes| Routing SubRouteCheck -->|No| NextRoute NextRoute --> RouteLeft{Are there other routes?} RouteLeft -->|Yes| Routing RouteLeft -->|No| NoMatchRoute[Routing match failed] PathCheck -->|Yes| MatchSuccess[Routing match successful] MatchSuccess --> CollectHandlers[Collect all Handlers from the matching chain] end %% Handler Execution Phase subgraph HandlerPhase[2. Handler Execution Phase] CollectHandlers --> ExecHandlers[Execute Handlers sequentially] ExecHandlers --> ErrorCheck{Error or redirect during execution?} ErrorCheck -->|No| FinishHandlers[All Handlers executed] ErrorCheck -->|Yes| StatusCheck{Is status code an error and Body unset or Error?} StatusCheck -->|Yes| EnterCatcher[Enter error-catching phase] StatusCheck -->|No| SkipCatcher[Skip error-catching phase] end %% Error-Catching Phase subgraph CatcherPhase[3. Error-Catching Phase] EnterCatcher --> CatcherHandlers[Execute Catcher's Handlers sequentially] NoMatchRoute --> CatcherHandlers CatcherHandlers --> DefaultHandler[DefaultGoal or custom error handling] end %% Final Response FinishHandlers --> FinalConvert[Service converts Salvo Response to Hyper Response] SkipCatcher --> FinalConvert DefaultHandler --> FinalConvert FinalConvert --> End[Return response to client] end %% Styling class RoutingPhase,HandlerPhase,CatcherPhase phase; class MainGraph mainGraph; classDef mainGraph fill:#f5f5f5,stroke:#333,stroke-width:1px; classDef phase fill:#e6f3ff,stroke:#333,stroke-width:2px; ``` --- url: /guide/topics/send-file.md --- # Sending Files Salvo can send files in several ways: ### NamedFile Salvo provides `salvo::fs::NamedFile`, which can be used to efficiently send files to clients. It does not load the entire file into memory; instead, it reads and sends only the portions requested by the client based on the `Range` header. In practice, using `Response::send_file` is a simplified way to utilize `NamedFile`. If you need more control over file delivery, you can use `NamedFileBuilder`. You can create a `NamedFileBuilder` via `NamedFile::builder`: ```rust #[handler] async fn send_file(req: &mut Request, res: &mut Response) { let builder = NamedFile::builder("/file/to/path"); } ``` After configuring the builder, you can send the file: ```rust #[handler] async fn send_file(req: &mut Request, res: &mut Response) { NamedFile::builder("/file/to/path").attached_name("image.png").send(req.headers(), res).await; } ``` ### Serve Static Middleware for serving static files or embedded files. - `StaticDir` provides support for serving static files from local directories. You can pass a list of multiple directories as arguments. For example: **main.rs** ```rust file="/codes/static-dir-list/src/main.rs" use salvo::prelude::*; use salvo::serve_static::StaticDir; #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); let router = Router::with_path("{*path}").get( StaticDir::new([ "static-dir-list/static/boy", "static-dir-list/static/girl", "static/boy", "static/girl", ]) .include_dot_files(false) .defaults("index.html") .auto_list(true), ); let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } ``` **Cargo.toml** ```toml file="/codes/static-dir-list/Cargo.toml" [package] name = "example-static-dir-list" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true, features = ["serve-static"] } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` If a file is not found in the first directory, it will be searched for in the second directory. - Provides support for `rust-embed`. For example: **main.rs** ```rust file="/codes/static-embed-files/src/main.rs" use rust_embed::RustEmbed; use salvo::prelude::*; use salvo::serve_static::static_embed; #[derive(RustEmbed)] #[folder = "static"] struct Assets; #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); let router = Router::with_path("{*path}").get(static_embed::().fallback("index.html")); let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } ``` **Cargo.toml** ```toml file="/codes/static-embed-files/Cargo.toml" [package] name = "example-static-embed-files" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] rust-embed.workspace = true salvo = { workspace = true, features = ["serve-static"] } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` The `with_fallback` method allows you to specify a fallback file to serve when the requested file is not found. This is particularly useful for single-page applications (SPAs). --- url: /guide/topics/testing.md --- # Writing Tests ## The Importance of Testing Writing tests is a hallmark of responsible engineering and the secret to sleeping soundly. A comprehensive test suite not only improves code quality and prevents regression errors but also ensures you rest easy after deployment. While your colleagues might be jolted awake by late-night system crash alerts, your application remains rock-solid—this is the tranquility and confidence that testing brings. ## Salvo Testing Tools The test module provided by Salvo helps you test Salvo-based projects. [Latest Documentation](https://docs.rs/salvo_core/latest/salvo_core/test/index.html) **Simple Example:** ```rust use salvo::prelude::*; #[handler] async fn hello_world() -> &'static str { "Hello World" } #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); tracing::info!("Listening on http://127.0.0.1:8698"); let acceptor = TcpListener::new("127.0.0.1:8698").bind().await; Server::new(acceptor).serve(route()).await; } fn route() -> Router { Router::new().get(hello_world) } #[cfg(test)] mod tests { use salvo::prelude::*; use salvo::test::{ResponseExt, TestClient}; #[tokio::test] async fn test_hello_world() { let service = Service::new(super::route()); let content = TestClient::get(format!("http://127.0.0.1:8698/")) .send(&service) .await .take_string() .await .unwrap(); assert_eq!(content, "Hello World"); } } ``` --- url: /guide/topics/use-template-engine.md --- # Using Template Engines Salvo does not come with any built-in template engine, as preferences for template engine styles vary from person to person. At its core, a template engine is simply: data + template = string. Therefore, as long as the final string can be rendered, any template engine can be supported. For example, support for `askama`: **main.rs** ```rust file="/codes/template-askama/src/main.rs" use askama::Template; use salvo::prelude::*; #[derive(Template)] #[template(path = "hello.html")] struct HelloTemplate<'a> { name: &'a str, } #[handler] async fn hello(req: &mut Request, res: &mut Response) { let hello_tmpl = HelloTemplate { name: req.param::<&str>("name").unwrap_or("World"), }; res.render(Text::Html(hello_tmpl.render().unwrap())); } #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); let router = Router::with_path("{name}").get(hello); let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } ``` **Cargo.toml** ```toml file="/codes/template-askama/Cargo.toml" [package] name = "example-template-askama" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] askama = "0.11" salvo = { workspace = true } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true ``` **templates/hello.html** ```html file="/codes/template-askama/templates/hello.html" Hello, {{ name }}! ``` **Note**: For projects that are not particularly complex, we highly recommend adopting a frontend-backend separation approach. Use more flexible and ecosystem-rich UI frameworks (such as React, Vue, Svelte, etc.) to build the frontend, with Salvo serving as the backend API service. This approach leads to higher development efficiency, clearer responsibilities between frontend and backend, and better aligns with modern web application development trends. --- url: /guide/topics/working-with-database.md --- # Using Databases ### [Diesel](https://diesel.rs/) ```rust use diesel::prelude::*; use diesel::r2d2::{ConnectionManager, Pool, PoolError, PooledConnection}; use once_cell::sync::OnceCell; use salvo::prelude::*; const DB_URL: &str = "postgres://benchmarkdbuser:benchmarkdbpass@tfb-database/hello_world"; type PgPool = Pool>; static DB_POOL: OnceCell = OnceCell::new(); fn connect() -> Result>, PoolError> { DB_POOL.get().unwrap().get() } fn build_pool(database_url: &str, size: u32) -> Result { let manager = ConnectionManager::::new(database_url); diesel::r2d2::Pool::builder() .max_size(size) .min_idle(Some(size)) .test_on_check_out(false) .idle_timeout(None) .max_lifetime(None) .build(manager) } fn main() { DB_POOL .set(build_pool(&DB_URL, 10).expect(&format!("Error connecting to {}", &DB_URL))) .ok(); } #[handler] async fn show_article(req: &mut Request, res: &mut Response) -> Result<(), Error> { let id: i64 = req.param::("id").unwrap_or_default(); let conn = connect()?; let article = articles::table.find(id).first::
(&conn)?; res.render(Json(row)); Ok(()) } ``` ### [Sqlx](https://github.com/launchbadge/sqlx) **main.rs** ```rust file="/codes/db-postgres-sqlx/src/main.rs" use std::sync::OnceLock; use salvo::prelude::*; use serde::Serialize; use sqlx::{FromRow, PgPool}; // Global PostgreSQL connection pool instance static POSTGRES: OnceLock = OnceLock::new(); // Helper function to get the PostgreSQL connection pool #[inline] pub fn get_postgres() -> &'static PgPool { POSTGRES.get().unwrap() } // User model representing the database table structure // Implements FromRow for SQL query results and Serialize for JSON responses #[derive(FromRow, Serialize, Debug)] pub struct User { pub id: i64, pub username: String, pub password: String, } // Handler for retrieving a user by ID from the database #[handler] pub async fn get_user(req: &mut Request, res: &mut Response) { // Extract user ID from query parameters let uid = req.query::("uid").unwrap(); // Execute SQL query to fetch user by ID let data = sqlx::query_as::<_, User>("select * from users where id = $1") .bind(uid) .fetch_one(get_postgres()) .await .unwrap(); // Return user data as JSON response res.render(serde_json::to_string(&data).unwrap()); } #[tokio::main] async fn main() { // Initialize logging system tracing_subscriber::fmt().init(); // Configure PostgreSQL connection let postgres_uri = "postgres://postgres:password@localhost/test"; // Create and initialize connection pool let pool = PgPool::connect(postgres_uri).await.unwrap(); // Store pool in global state POSTGRES.set(pool).unwrap(); // Configure router with user endpoint: // - GET /users?uid={id} : Get user by ID let router = Router::with_path("users").get(get_user); // Start server on port 8698 let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } ``` **Cargo.toml** ```toml file="/codes/db-postgres-sqlx/Cargo.toml" [package] name = "example-db-postgres-sqlx" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] salvo = { workspace = true } tokio = { workspace = true, features = ["macros"] } tracing.workspace = true tracing-subscriber.workspace = true sqlx = { workspace = true, features = ["runtime-tokio-native-tls", "postgres"] } serde = { workspace = true, features = ["derive"] } serde_json.workspace = true ``` ### [rbatis](https://github.com/rbatis/rbatis) **main.rs** ```rust file="/codes/db-mysql-rbatis/src/main.rs" #[macro_use] extern crate rbatis; extern crate rbdc; use std::sync::LazyLock; use rbatis::RBatis; use rbdc_mysql::driver::MysqlDriver; use salvo::prelude::*; use serde::{Deserialize, Serialize}; // Global RBatis instance for database operations pub static RB: LazyLock = LazyLock::new(RBatis::new); // User model representing the database table structure #[derive(Clone, Debug, Serialize, Deserialize)] pub struct User { pub id: i64, pub username: String, pub password: String, } // Implement select query for User model // Generates SQL: SELECT * FROM user WHERE id = #{id} LIMIT 1 impl_select!(User{select_by_id(id:String) -> Option => "`where id = #{id} limit 1`"}); // Handler for retrieving a user by ID from the database #[handler] pub async fn get_user(req: &mut Request, res: &mut Response) { // Extract user ID from query parameters let uid = req.query::("uid").unwrap(); // Execute select query and get user data let data = User::select_by_id(&*RB, uid.to_string()).await.unwrap(); println!("{data:?}"); // Return user data as JSON response res.render(serde_json::to_string(&data).unwrap()); } #[tokio::main] async fn main() { // Initialize logging system tracing_subscriber::fmt().init(); // Configure MySQL connection let mysql_uri = "mysql://root:123456@localhost/test"; // Initialize RBatis with MySQL driver and connection URI RB.init(MysqlDriver {}, mysql_uri).unwrap(); // Configure router with user endpoint: // - GET /users?uid={id} : Get user by ID let router = Router::with_path("users").get(get_user); // Start server on port 8698 let acceptor = TcpListener::new("0.0.0.0:8698").bind().await; Server::new(acceptor).serve(router).await; } ``` **Cargo.toml** ```toml file="/codes/db-mysql-rbatis/Cargo.toml" [package] name = "example-db-mysql-rbatis" version.workspace = true edition.workspace = true publish.workspace = true rust-version.workspace = true [dependencies] async-std = { workspace = true } rbatis = { workspace = true } rbdc = { workspace = true } rbdc-mysql = { workspace = true } rbs = { workspace = true } salvo = { workspace = true } serde = { workspace = true, features = ["derive"] } tokio = { workspace = true, features = ["macros"] } tracing = { workspace = true } tracing-subscriber = { workspace = true } serde_json = { workspace = true } ``` ## [SeaORM](https://www.sea-ql.org/SeaORM/) SeaORM is an asynchronous, dynamic ORM that provides robust relational database support, including entity relationships, a migration system, and a comprehensive type-safe query builder. It is well-suited for medium to large-scale projects requiring a full-featured ORM. ## [Tokio ORM (Toasty)](https://github.com/tokio-rs/toasty) Toasty is an ORM developed by the Tokio team, currently under active development. It focuses on providing a tightly integrated ORM solution for the Tokio ecosystem. It may be suitable for projects that rely on Tokio and are open to adopting emerging technologies. ## [SurrealDB Rust SDK](https://surrealdb.com/docs/sdk/rust) The SurrealDB Rust SDK provides connectivity to this multi-model database, making it suitable for applications that need to handle graph data, document data, and relational data. It is an excellent choice for projects requiring flexible data models. --- url: /donate.md --- # Sponsorship Project Our meeting here is a stroke of fate. In the vast expanse of the internet, the probability of our encounter is even smaller than the chance of us being born into this world. How about supporting the project a little? Let me remember you, and let your generosity spur me forward when I feel lazy, encouraging me to keep updating the project and making it better and better. You can transfer funds directly via Alipay or WeChat, or [**Buy Me a Coffee ☕**](https://ko-fi.com/chrislearn): | Alipay | WeChat | | ----------------------------- | ----------------------------- | | ![Alipay](/images/alipay.png) | ![WeChat](/images/weixin.png) | --- url: /index.md --- # Salvo A Perfect Rust Web Framework > Simple yet powerful, you'll fall in love at first sight [Quick Start](./guide/quick-start) | [Donate](./donate) ## Features - ❤️ **So Simple You'll Fall in Love Instantly**: While other Rust frameworks leave you wrestling with complex language features and type gymnastics, often failing to compile, Salvo is intuitive and easy to master—whether you're a beginner or a seasoned developer. - 🛠️ **Powerful and Practical Features**: Despite its simplicity, Salvo remains robust and feature-rich, offering built-in support for Multipart, OpenAPI, HTTP2/3, LetsEncrypt, flexible data parsing, and many other practical functionalities. - 🚀 **Lightning-Fast Performance**: Powered by Rust, Salvo delivers exceptional performance. Compared to frameworks in most other languages, it's like they're firing a pistol one shot at a time, while you unleash a sustained barrage with a heavy machine gun. - 🧭 **A Routing System Like No Other**: Salvo features a unique routing system that supports infinite nesting, offering convenience, flexibility, and efficiency. Use it however you like—nest it, navigate anywhere—and experience unparalleled freedom and exhilaration. - 🔌 **Minimalist Middleware System**: In Salvo, middleware and handlers are unified as Handlers, creating a harmonious and streamlined approach. The framework also provides a rich and flexible set of official middleware implementations to meet diverse application needs. - 🛡️ **Rock-Solid Stability**: Rust's safety mechanisms ensure your website runs reliably without crashes, giving you peace of mind. Spend more time enjoying life and less time frantically fixing servers.