Salvo is a powerful and simplest web server framework in Rust world

Overview

Salvo is an extremely simple and powerful Rust web backend framework. Only basic Rust knowledge is required to develop backend services.

🎯 Features

  • Built with Hyper and Tokio;
  • Unified middleware and handle interface;
  • Routing supports multi-level nesting, and middleware can be added at any level;
  • Integrated Multipart form processing;
  • Support Websocket;
  • Acme support, automatically get TLS certificate from let's encrypt;
  • Supports mapping from multiple local directories into one virtual directory to provide services.

⚡️ Quick start

You can view samples here, or view offical website.

Create a new rust project:

cargo new hello_salvo --bin

Add this to Cargo.toml

[dependencies]
salvo = { version = "0.22", features = ["full"] }
tokio = { version = "1", features = ["full"] }

Create a simple function handler in the main.rs file, we call it hello_world, this function just render plain text "Hello World".

use salvo::prelude::*;

#[fn_handler]
async fn hello_world(res: &mut Response) {
    res.render(Text::Plain("Hello World"));
}

In the main function, we need to create a root Router first, and then create a server and call it's bind function:

use salvo::prelude::*;

#[fn_handler]
async fn hello_world() -> &'static str {
    "Hello World"
}
#[tokio::main]
async fn main() {
    let router = Router::new().get(hello_world);
    Server::new(TcpListener::bind("127.0.0.1:7878")).serve(router).await;
}

Middleware

There is no difference between Handler and Middleware, Middleware is just Handler. So you can write middlewares without to know concpets like associated type, generic type. You can write middleware if you can write function!!!*

Chainable tree routing system

Normally we write routing like this:

") .get(show_article) .patch(edit_article) .delete(delete_article);">
Router::with_path("articles").get(list_articles).post(create_article);
Router::with_path("articles/")
    .get(show_article)
    .patch(edit_article)
    .delete(delete_article);

Often viewing articles and article lists does not require user login, but creating, editing, deleting articles, etc. require user login authentication permissions. The tree-like routing system in Salvo can meet this demand. We can write routers without user login together:

").get(show_article));">
Router::with_path("articles")
    .get(list_articles)
    .push(Router::with_path("").get(show_article));

Then write the routers that require the user to login together, and use the corresponding middleware to verify whether the user is logged in:

").patch(edit_article).delete(delete_article));">
Router::with_path("articles")
    .hoop(auth_check)
    .post(list_articles)
    .push(Router::with_path("").patch(edit_article).delete(delete_article));

Although these two routes have the same path("articles"), they can still be added to the same parent route at the same time, so the final route looks like this:

").get(show_article)), ) .push( Router::with_path("articles") .hoop(auth_check) .post(list_articles) .push(Router::with_path("").patch(edit_article).delete(delete_article)), );">
Router::new()
    .push(
        Router::with_path("articles")
            .get(list_articles)
            .push(Router::with_path("").get(show_article)),
    )
    .push(
        Router::with_path("articles")
            .hoop(auth_check)
            .post(list_articles)
            .push(Router::with_path("").patch(edit_article).delete(delete_article)),
    );

matches a fragment in the path, under normal circumstances, the article id is just a number, which we can use regular expressions to restrict id matching rules, r"".

You can also use <*> or <**> to match all remaining path fragments. In order to make the code more readable, you can also add appropriate name to make the path semantics more clear, for example: <**file_path>.

File upload

We can get file async by the function file in Request:

#[fn_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.filename().unwrap_or_else(|| "file".into()));
        if let Err(e) = tokio::fs::copy(&file.path, Path::new(&dest)).await {
            res.set_status_code(StatusCode::INTERNAL_SERVER_ERROR);
        } else {
            res.render("Ok");
        }
    } else {
        res.set_status_code(StatusCode::BAD_REQUEST);
    }
}

More Examples

Your can find more examples in examples folder. You can run these examples with the following command:

cargo run --bin --example-basic_auth

You can use any example name you want to run instead of basic_auth here.

There is a real and open source project use Salvo: https://github.com/driftluo/myblog.

🚀 Performance

Benchmark testing result can be found from here:

https://web-frameworks-benchmark.netlify.app/result?l=rust

https://www.techempower.com/benchmarks/#section=test&runid=785f3715-0f93-443c-8de0-10dca9424049 techempower

🩸 Contributing

Contributions are absolutely, positively welcome and encouraged! Contributions come in many forms. You could:

  • Submit a feature request or bug report as an issue;
  • Comment on issues that require feedback;
  • Contribute code via pull requests;
  • Publish Salvo-related technical articles on blogs or technical platforms。

All pull requests are code reviewed and tested by the CI. Note that unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in Salvo by you shall be dual licensed under the MIT License, without any additional terms or conditions.

Supporters

Salvo is an open source project. If you want to support Salvo, you can buy a coffee here.

⚠️ License

Salvo is licensed under either of

Comments
  • http 2 protocol

    http 2 protocol

    Is your feature request related to a problem? Please describe. salvo support HTTP 2 version?

    Describe the solution you'd like

    • salvo = {version = "0.37.1", features = ["basic-auth", "test"]}
    • my client k6 upgrades the connection to HTTP/2.0 but salvo response is HTTP/1.1

    Describe alternatives you've considered A clear and concise description of any alternative solutions or features you've considered.

    Additional context Add any other context or screenshots about the feature request here.

    opened by sysmat 10
  • Add support for caching headers primarily ETag/LastModified/If-Non-Match

    Add support for caching headers primarily ETag/LastModified/If-Non-Match

    I would like to set appropriate caching headers for any responses include html and apis besides file. This improves performance on slow network when fetching it again.

    I would like to have similar feature to trillium's caching-headers.

    (In case you are wondering why I'm filing issue, I'm trying to port by blog from trillium to salvo https://github.com/prabirshrestha/rblog with the main reason being trillium not support first class error handling compared to salvo)

    opened by prabirshrestha 10
  • Route Guard mechanism

    Route Guard mechanism

    Middleware can be deployed for authentication. However having route guard type of mechanism can help greatly in organizing authorization code.

    Implement additional guard on routes

    Similar functionality is already available in Actix which helps a lot in properly organizing the authorization logic. At the point of route guard processing, the user might already have authenticated and the the guard only decides if the user can access the route based on the data in the request.

    opened by Murtuzakabul 7
  • Add support for flash message

    Add support for flash message

    This is similar to one popularized by the ruby on rails. https://www.rubyguides.com/2019/11/rails-flash-messages/

    rust counter parts:

    • https://docs.rs/axum-flash/latest/axum_flash/
    • https://lib.rs/crates/tide-flash
    • https://github.com/LukeMathWalker/actix-web-flash-messages

    Initially I'm looking to show flash message when the user sign up or resets the password.

    Noticed that one of the sample using it but don't see how it is getting set in the rust world. https://github.com/salvo-rs/salvo/blob/9524d95dd7112dc598fec4ec12e88ad13a210fc9/examples/db-sea-orm/templates/index.html.tera#L5-L8

    opened by prabirshrestha 6
  • Error using x86_64-PC-Windows-GNU on Windows

    Error using x86_64-PC-Windows-GNU on Windows

    Describe the bug Error using x86_64-PC-Windows-GNU on Windows

    To Reproduce Steps to reproduce the behavior:

    1. Cargo.toml add salvo_extra
    2. Add LogHandler to Hoop
    3. cargo run .

    Expected behavior Normal compilation

    Salvo Version salvo = "0.22" salvo_extra = "0.22"

    Screenshots 微信截图_20220509152432

    opened by liaohongxing 5
  • Compile error

    Compile error

    $ cargo build --release
    
    Compiling salvo_core v0.13.3 (/media/data1/rust/salvo/core)
    error[E0433]: failed to resolve: use of undeclared type `StatusCode`
      --> core/src/error.rs:57:29
       |
    57 |         res.set_status_code(StatusCode::INTERNAL_SERVER_ERROR);
       |                             ^^^^^^^^^^ use of undeclared type `StatusCode`
    
    opened by armersong 5
  • data support

    data support

    Is your feature request related to a problem? Please describe. Adding a database to a salvo project requires unsafe rust as in the case of the https://github.com/driftluo/myblog/blob/master/src/db_wrapper.rs#L416 website featured in the readme

    Describe the solution you'd like A way to easily connect a database to salvo maybe like how actix does it would make things alot easier.

    opened by Tricked-dev 5
  • Extraction depends on struct order.

    Extraction depends on struct order.

    Describe the bug Successful extraction of a request's param depends on the struct's fields order.

    To Reproduce Given the following code:

    #[handler]
    pub async fn update_post(req: &mut Request, res: &mut Response) -> AppResult<()> {
        let id = req.param::<String>("slug").unwrap();
        let mut update_post_request: UpdatePostRequest = req.extract().await?;
        update_post_request.id = Some(id);
        res.render(Json(true));
    }
    

    The following struct gives me an error: SalvoParse(Deserialize(Error("missing field title")))

    #[derive(Debug, Extractible, Serialize, Deserialize)]
    #[extract(default_source(from = "body", format = "json"))]
    struct UpdatePostRequest {
        id: Option<String>,
        title: String,
        teaser: String,
        content: String,
    }
    

    It works when using

    #[derive(Debug, Extractible, Serialize, Deserialize)]
    #[extract(default_source(from = "body", format = "json"))]
    struct UpdatePostRequest {
        title: String,
        teaser: String,
        content: String,
        id: Option<String>,
    }
    

    Expected behavior Should work in both cases.

    opened by JohnnyMcWeed 4
  • Function name update in TcpListener

    Function name update in TcpListener

    For every examples the keyword new in TcpListener are here but in the lastest version they doesn't exist and i think it's the same for the testing env

    opened by Limerio 4
  • compression doesn't seem to work as expected

    compression doesn't seem to work as expected

    Not sure if this is the expected or not but does seem weird that it doesn't behave as I thought it would. There are two bugs I found.

    1. No compression when sending str.
    #[handler]
    async fn hello_world(res: &mut Response) {
      res.render("hello");
    }
    
    let router = Router::new()
      .hoop(extra::compression::brotli())
      .get(hello_world);
    

    I see compression when sending file called hello.txt with contents hello.

    let router = Router::new()
      .hoop(extra::compression::brotli())
      .get(extra::serve_static::FileHandler::new("./hello.txt"));
    
    1. Doesn't read accept-encoding request header and always returns br since I set it as extra::compression::brotli. Do note that browsers can send accept-encoding which is comma separated (accept-encoding: gzip, deflate, br) as is usually the preferred encoding algorithm they expect but server can decide to return any.
    curl -vv http://localhost:8080 -H 'Accept-Encoding: gzip'
    
    *   Trying 127.0.0.1:8080...
    * Connected to localhost (127.0.0.1) port 8080 (#0)
    > GET / HTTP/1.1
    > Host: localhost:8080
    > User-Agent: curl/7.84.0
    > Accept: */*
    > Accept-Encoding: gzip
    >
    * Mark bundle as not supporting multiuse
    < HTTP/1.1 200 OK
    < content-disposition: inline
    < content-type: text/plain; charset=utf-8
    < last-modified: Mon, 05 Sep 2022 07:55:20 GMT
    < etag: "6fbaf-6-6315ab68-1c107dc6"
    < accept-ranges: bytes
    < content-encoding: br
    < transfer-encoding: chunked
    < date: Mon, 05 Sep 2022 08:05:36 GMT
    <
    hello
    

    I would had thought I would specify compress as just compression() instead of algorithm. By default it should register all 3 algorithms and based on the Accept-Encoding header it should compress accordingly.

    I was expecting it to work similar to trillium's implementation of compression handler which most of the other frameworks do too. https://github.com/trillium-rs/trillium/blob/4ece63c91f6394efef9f48cb060fe7b9a964ce3a/compression/src/lib.rs

    opened by prabirshrestha 4
  • WebSocket Example on homepage no longer working

    WebSocket Example on homepage no longer working

    Describe the bug I recently updated salvo from 1.16 to 1.29. Sadly my webSocket server would no longer compile. So I went grab the example at the bottom of salvo homepage, that one ain't compiling as well.

    Cargo.toml:

    [package]
    name = "noname-server"
    version = "0.1.0"
    edition = "2021"
    
    # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
    
    [dependencies]
    salvo = { version = "0.*", features = ["full"] }
    tokio = { version = "1", features = ["full"] }
    tokio-stream = "*"
    futures = "*"
    futures-util = "0.3"
    once_cell = "1"
    tracing = "0.1"
    tracing-subscriber = "0.2.0"
    serde = { version = "1.0", features = ["derive"] }
    serde_json = "1"
    rand = "0.8"
    thiserror = "1"
    

    Error:

    error[E0412]: cannot find type `HttpError` in this scope
     --> src/main.rs:6:71
      |
    6 | async fn connect(req: &mut Request, res: &mut Response) -> Result<(), HttpError> {
      |                                                                       ^^^^^^^^^ not found in this scope
    
    error[E0698]: type inside `async` block must be known in this context
     --> src/main.rs:5:1
      |
    5 | #[handler]
      | ^^^^^^^^^^ cannot infer type
      |
    note: the type is part of the `async` block because of this `await`
     --> src/main.rs:5:1
      |
    5 | #[handler]
      | ^^^^^^^^^^
      = note: this error originates in the attribute macro `handler` (in Nightly builds, run with -Z macro-backtrace for more info)
    
    Some errors have detailed explanations: E0412, E0698.
    For more information about an error, try `rustc --explain E0412`.
    error: could not compile `noname-server` due to 2 previous errors
    

    To Reproduce Steps to reproduce the behavior:

    1. Go to '...'
    2. Click on '....'
    3. Scroll down to '....'
    4. See error

    Expected behavior The example should compile(hopefully).

    Screenshots image

    Desktop (please complete the following information):

    • OS: macos
    • Version 0.29.1
    opened by xiaoas 4
  • Uploading req body as stream

    Uploading req body as stream

    I'm looking to support uploading files but without req.file() since that uses the forms.

    Is there some way to get a ReaderStream for the request body so I can use tokio::io::copy(req_body, dest_stream)?

    opened by prabirshrestha 0
  • Supply depot parameter for `BasicAuthValidator::validate`

    Supply depot parameter for `BasicAuthValidator::validate`

    Currently, BasicAuthValidator::validate only supplies username and password, and returning true will enter the handler for that request. However, before entering the handler, we want to record some information regarding the current account, such as account level, privilege, or something else we want to use in the handler.

    Change async fn validate(&self, username: &str, password: &str) -> bool; to async fn validate(&self, username: &str, password: &str, depot: &mut Depot) -> bool; would be helpful to implement these functions.

    opened by xmh0511 1
  • `higher-ranked lifetime error` while spawning server in Tokio

    `higher-ranked lifetime error` while spawning server in Tokio

    Describe the bug When trying to not configure and start the Salvo server on the main thread but in a spawned thread the compiler fails with a higher-ranked lifetime error and messages like note: could not prove [async block@crate/server/src/server_builder.rs:115:22: 120:10]: Send.

    To Reproduce Steps to reproduce the behavior:

            let router = self.router()?;
            let rustls_config = rustls_config();
            let (tx, rx) = oneshot::channel();
    
            let runtime = tokio::runtime::Builder::new_multi_thread()
                .enable_all()
                .build()
                .unwrap();
    
            let _guard = runtime.enter();
    
            let create_acceptor = async move {
                let listener = TcpListener::new(local_address.clone()).rustls(rustls_config.clone());
    
                let acceptor = QuinnListener::new(rustls_config, local_address.clone())
                    .join(listener)
                    .try_bind()
                    .await?;
    
                Ok::<_, Error>(acceptor)
            };
    
            let acceptor = runtime.block_on(create_acceptor)?;
    
            let server = salvo::Server::new(acceptor);
    
            let serve1 = server.serve_with_graceful_shutdown(
                router,
                async {
                    tracing::info!("Waiting for server to stop");
                    rx.await.ok();
                },
                None,
            );
    
            let serve2 = async move {
                tracing::info!("server.await begin");
                // let x = force_send_sync::Send::new(serve);
                serve1.await;
                tracing::info!("server.await end");
            };
    
            runtime.spawn(serve2);  // <<< This is the line where the error occurs.
    

    Additional context

    • rustc 1.68.0-nightly
    opened by jgeluk 11
  • warning shouldn't be set when status code is 204 no content

    warning shouldn't be set when status code is 204 no content

    If I send a status code with 204 no content, it shouldn't show the following warning since it has no content, there is no need to set the content type header. This pollutes the logs.

    2022-12-31T09:15:49.118458Z  WARN salvo_core::service: Http response content type header not set uri=/someurl method="DELETE"
    
    opened by prabirshrestha 0
  • Proxy Compression Bug

    Proxy Compression Bug

    I have created a minimal repro so it is easier for you to try it out.

    https://github.com/prabirshrestha/salvo-proxy-compression-bug

    Basically when using compression with proxy there are issues which renders garbage instead of actual content. Could be double compression one on the proxy server by create react app and the other one by salvo?

    Another potentially bug I found was that there is no vary header set when using compression so caching headers might not work.

    This bug only repros in Safari on iphone and ipad. Chrome/firefox seems to be be able to handle this correctly so you might not repro it there. Might be non safari browser is smarter to autodetect dual compression and decompress it twice? I have not debugged it thoroughly so not sure if there is multiple compression happening or some other proxy settings that is creating this issue.

    For future ref:

    use salvo::prelude::*;
    
    #[handler]
    async fn hello() -> &'static str {
        "Hello World"
    }
    
    #[tokio::main]
    async fn main() {
        tracing_subscriber::fmt().init();
    
        // CachingHeader must be before Compression.
        let router = Router::with_hoop(CachingHeaders::new())
            .hoop(Compression::new().with_min_length(0))
            .push(Router::with_path("/server").get(hello))
            .push(Router::with_path("<**rest>").handle(spa()));
    
        let acceptor = TcpListener::bind("0.0.0.0:7878");
        Server::new(acceptor).serve(router).await;
    }
    
    fn spa() -> salvo::proxy::Proxy<Vec<&'static str>> {
        salvo::proxy::Proxy::new(vec!["http://localhost:3000"])
    }
    

    Copy pasting caching-header example to the new repro doesn't seem to compile. I had to change let acceptor = TcpListener::new("127.0.0.1:7878").bind().await; to let acceptor = TcpListener::bind("0.0.0.0:7878"); instead.

    opened by prabirshrestha 2
  • Expose basic auth functions without creating BasicAuth struct

    Expose basic auth functions without creating BasicAuth struct

    I need to write custom basic auth for my webdav where I want certain paths to be accessibly anonymously and some with auth. In order to do this I want to have let (username, password) = basic_auth::parse_credentials(&req); and basic_auth::ask_credential(&mut res). Is it possible to expose these as functions that I can use directly? https://github.com/salvo-rs/salvo/blob/fe78cb388f1b06f645b361b3ec781f4167b802a1/crates/extra/src/basic_auth.rs#L47-L66

    opened by prabirshrestha 4
Static Web Server - a very small and fast production-ready web server suitable to serve static web files or assets

Static Web Server (or SWS abbreviated) is a very small and fast production-ready web server suitable to serve static web files or assets.

Jose Quintana 496 Jan 2, 2023
🚀Memory safe, blazing fast, configurable, minimal hello world written in rust(🚀) in a few lines of code with few(1092🚀) dependencies🚀

?? hello-world.rs ?? ?? Memory safe, blazing fast, minimal and configurable hello world project written in the rust( ?? ) programming language ?? ?? W

mTvare 2.7k Jan 7, 2023
Archibald is my attempt at learning Rust and writing a HTTP 1.1 web server.

Archibald To be a butler, is to be able to maintain an even-temper, at all times. One must have exceptional personal hygiene and look sharp and profes

Daniel Cuthbert 4 Jun 20, 2022
VRS is a simple, minimal, free and open source static web server written in Rust

VRS is a simple, minimal, free and open source static web server written in Rust which uses absolutely no dependencies and revolves around Rust's std::net built-in utility.

null 36 Nov 8, 2022
Sincere is a micro web framework for Rust(stable) based on hyper and multithreading

The project is no longer maintained! Sincere Sincere is a micro web framework for Rust(stable) based on hyper and multithreading. Style like koa. The

null 94 Oct 26, 2022
A blazingly fast static web server with routing, templating, and security in a single binary you can set up with zero code. :zap::crab:

binserve ⚡ ?? A blazingly fast static web server with routing, templating, and security in a single binary you can set up with zero code. ?? UPDATE: N

Mufeed VH 722 Dec 27, 2022
Simple and fast web server

see Overview Simple and fast web server as a single executable with no extra dependencies required. Features Built with Tokio and Hyper TLS encryption

null 174 Dec 9, 2022
Operator is a web server. You provide a directory and Operator serves it over HTTP.

Operator Operator is a web server. You provide a directory and Operator serves it over HTTP. It serves static files the way you'd expect, but it can a

Matt Kantor 6 Jun 6, 2022
A flexible web framework that promotes stability, safety, security and speed.

A flexible web framework that promotes stability, safety, security and speed. Features Stability focused. All releases target stable Rust. This will n

Gotham 2.1k Jan 3, 2023
A toy web framework inspired by gin-gonic/gin and expressjs/express.

Rum Framework A toy web framework inspired by gin-gonic/gin and expressjs/express. Installation Just add rum_framework to the dependencies of Cargo.to

Hidetomo Kou(YingZhi 2 Dec 20, 2022
Web Server made with Rust - for learning purposes

Web Server made with Rust - for learning purposes

Lílian 2 Apr 25, 2022
An Extensible, Concurrent Web Framework for Rust

Iron Extensible, Concurrency Focused Web Development in Rust. Response Timer Example Note: This example works with the current iron code in this repos

null 6.1k Dec 27, 2022
An expressjs inspired web framework for Rust

nickel.rs nickel.rs is a simple and lightweight foundation for web applications written in Rust. Its API is inspired by the popular express framework

null 3k Jan 3, 2023
A web framework for Rust.

Rocket Rocket is an async web framework for Rust with a focus on usability, security, extensibility, and speed. #[macro_use] extern crate rocket; #[g

Sergio Benitez 19.5k Jan 8, 2023
A lightweight web framework built on hyper, implemented in Rust language.

Sapper Sapper, a lightweight web framework, written in Rust. Sapper focuses on ergonomic usage and rapid development. It can work with stable Rust. Sa

Daogang Tang 622 Oct 27, 2022
Web framework in Rust

Rouille, a Rust web micro-framework Rouille is a micro-web-framework library. It creates a listening socket and parses incoming HTTP requests from cli

Pierre Krieger 840 Jan 1, 2023
A fast, boilerplate free, web framework for Rust

Tower Web A web framework for Rust with a focus on removing boilerplate. API Documentation Tower Web is: Fast: Fully asynchronous, built on Tokio and

Carl Lerche 969 Dec 22, 2022
The light web framework for Rust.

Rusty Web Rusty web is a simple to use, fully customizable lightweight web framework for rust developers. Learn rusty web Installation [dependencies]

Tej Magar 5 Feb 27, 2024
web browser as a language server

web-browser-lsp A toy program that implements a text-based web browser as a language server. Motivation My favorite progrmming tools are neovim, tmux

octaltree 17 Nov 24, 2022