Making a web server

I want to make a web server using Rust, and am looking around for a suitable crate or crates to help out with the job. I am moderately familiar with the "Book" example.

Things I think I need:
Support for https.
http request parsing.
Multi-tasking ( to allow multiple requests to be handled at the same time ).

Things that might be nice:
Support for HTTP/2 and HTTP/3.

What else do I need to consider?
I guess there are quite a few competing possibilities.
Suggestions please on where to start!

1 Like

I'd recommend either axum, warp, rocket, or tide. If you want something lower level you can go for hyper.

1 Like

Thanks, those are all names I have come across. I am going to start with axum, see how I get on.

Axum is very young. I recommend: actix-web or rocket.
Check this:

2 Likes

I have spent an hour or so getting into axum. Main thing that has been catching me out is enabling crate features, I'm not used to having to do that, and when you fail to enable some feature, the errors don't give much clue what you did wrong ( lack of experience on my part I guess ).

My current toml file:

[package]
name = "axumtest"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
axum = { version = "0.3.2", features = ["multipart","headers"] }
hyper = { version = "0.14.14", features = ["full"] }
tokio = { version = "1.13.0", features = ["full"] }
tower = "0.4.10"
serde = { version = "1.0.130", features = ["derive"] }
headers = "0.3.5"

I am currently a bit stuck on Form, I just want a HashMap of the form values. How to get the cookies as a HashMap seems a bit of a puzzle as well at the moment. Anyway, it's an interesting learning experience for me. I have weird function declarations where I didn't even know about the syntax... and still don't, but it seems to work:

async fn my_get_handler(
    // What is going on here??
    Path(path): Path<String>,
    Query(params): Query<HashMap<String, String>>,
    TypedHeader(cookie): TypedHeader<Cookie>
) -> String {
    format!("Hi George path is '{}' and params are {:?} and cooke is {:?}", path, params, cookie )
}

One thing that may be a problem, it seems to want distinct routes for get and post. In other words if I do this, it complains about a conflict:

    let app = Router::new()
        .route("/*key", post(my_post_handler))
        .route("/*key", get(my_get_handler));

I'm not sure axum or any framework is really what I am looking for ( it brings in about 100 dependent crates!!). I just want something that can parse an http request, including cookies, multipart and url-encoded forms. Maybe it's easier to write it myself. I haven't seen much said about https either, but maybe I have missed that.

Maintainer of axum here. Happy to hear you're trying it out :blush:

I have spent an hour or so getting into axum. Main thing that has been catching me out is enabling crate features, I'm not used to having to do that, and when you fail to enable some feature, the errors don't give much clue what you did wrong

I recommend looking at the examples crates we have in the repo. They're all self contained crates that enables the right features.

The cargo features required to use certain types are also mentioned in the docs such as here which says " This is supported on crate feature headers only".

I am currently a bit stuck on Form, I just want a HashMap of the form values

Form can be used with any type that implements serde::Deserialize which HashMap<String, String> does. So Form<HashMap<String, String>> will work and gives you all the form params.

How to get the cookies as a HashMap seems a bit of a puzzle as well at the moment

axum doesn't have anything built-in for cookies. I recommend tower-cookies which works with axum out of the box.

I have weird function declarations where I didn't even know about the syntax... and still don't, but it seems to work:

This works because function arguments in Rust are actually also patterns. So you can do pattern matching directly there. And since Path, Query, TypedHeader etc are defined as tuple structs with one public field (ie Path<T>(T)) you can destructure them directly directly in the function arguments. You could also just do let path = path.0 inside the function but that gets a bit repetitive.

One thing that may be a problem, it seems to want distinct routes for get and post. In other words if I do this, it complains about a conflict:

That is done like so .route("/*key", post(post_handler).get(get_handler)) like shown here.

6 Likes

Thanks! That's all working out pretty well so far. Multipart and Form working perfectly, I haven't got round to the cookies yet. My silly program so far:

use axum::extract::{Form /*TypedHeader*/, Multipart, Path, Query};
use axum::response::Html;
use axum::routing::get;
use axum::Router;
use std::collections::HashMap;
//use headers::Cookie;

#[tokio::main]
async fn main() {
    // build our application with a single route
    let app = Router::new().route("/*key", get(my_get_handler).post(my_post_handler));
    // run it with hyper on localhost:3000
    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn my_get_handler(
    // Wow, fancy pattern matching parameters...
    Path(path): Path<String>,
    Query(params): Query<HashMap<String, String>>,
) -> Html<String> {
    let s = format!(
        "Hi George path is '{}' and params are {:?} \
         <form method=post enctype=\"multipart/form-data\"><input name=n>\
         <input name=f type=file>\
         </form>",
        path, params
    );

    Html(s)
}

async fn my_post_handler(
    // More fancy parameters...
    Path(path): Path<String>,
    Query(params): Query<HashMap<String, String>>,
    mp: Option<Multipart>,
    form: Option<Form<HashMap<String, String>>>,
) -> Html<String> {
    let mut mpinfo = String::new();
    if let Some(mut mp) = mp {
        mpinfo += "multipart form!!";
        while let Some(field) = mp.next_field().await.unwrap() {
            let name = field.name().unwrap().to_string();
            let filename = match field.file_name()
            {
              Some(s) => s.to_string(),
              None => "No filename".to_string()
            };
            let ct = match field.content_type()
            {
              Some(s) => s.to_string(),
              None => "".to_string()
            };
            let mut datalen = 0;
            let mut text = "".to_string();
            if ct == "" 
            { 
              match field.text().await
              {
                Ok(s) => text = s,
                Err(_) => {}
              }
            }
            else
            {
              datalen = match field.bytes().await
              {
                Ok(bytes) => bytes.len(),
                Err(_) => 0
              };
            }
            
            mpinfo += &format!(
                "<p>name is `{}` filename is `{}` ct is `{}` data len is {} bytes text is {}",
                name,
                filename,
                ct,
                datalen,
                text
            );
        }
    }
    let s = format!(
        "Hi George path is '{}' and params are {:?} form is {:?} mpinfo {}",
        path, params, form, mpinfo
    );
    Html( s )
}
2 Likes

I thought I would post an update on my progress with Axum, which I should say has worked flawlessly. It took a little while for me to figure out how to send a general response consisting of a status code, arbitrary headers and a body of bytes. That's the implementation of IntoResponse at the end of this code. This is my current program ( main.rs ):

use mimalloc::MiMalloc;

#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;

use axum::{
    extract::{Extension, Form, Multipart, Path, Query},
    routing::get,
    AddExtensionLayer, Router,
};

use tower::ServiceBuilder;
use tower_cookies::{CookieManagerLayer, Cookies};

use tokio::sync::{
    mpsc::{channel, Receiver, Sender},
    Mutex,
};

use database::{
    genquery::{GenQuery, Part},
    Database,
};

use std::{collections::HashMap, sync::Arc, thread};

struct ServerQuery {
    pub x: Box<GenQuery>,
}

impl ServerQuery {
    pub fn new() -> Self {
        Self {
            x: Box::new(GenQuery::new()),
        }
    }
}

struct SharedState {
    tx: Sender<ServerQuery>,
    rx: Receiver<ServerQuery>,
}

#[tokio::main]
async fn main() {
    let (tx, mut server_rx): (Sender<ServerQuery>, Receiver<ServerQuery>) = channel(1);
    let (server_tx, rx): (Sender<ServerQuery>, Receiver<ServerQuery>) = channel(1);

    // This is the server thread (synchronous).
    thread::spawn(move || {
        let stg = Box::new(database::stg::SimpleFileStorage::new(
            "c:\\Users\\pc\\rust\\sftest01.rustdb",
        ));
        let db = Database::new(stg, database::init::INITSQL);
        loop {
            let mut q = server_rx.blocking_recv().unwrap();
            let sql = "EXEC [handler].[".to_string() + &q.x.path + "]()";
            db.run_timed(&sql, &mut *q.x);
            let _x = server_tx.blocking_send(q);
            db.save();
        }
    });

    let state = Arc::new(Mutex::new(SharedState { tx, rx }));

    // build our application with a single route
    let app = Router::new()
        .route("/*key", get(my_get_handler).post(my_post_handler))
        .layer(
            ServiceBuilder::new()
                .layer(CookieManagerLayer::new())
                .layer(AddExtensionLayer::new(state)),
        );

    // run it with hyper on localhost:3000
    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn my_get_handler(
    Extension(state): Extension<Arc<Mutex<SharedState>>>,
    Path(path): Path<String>,
    Query(params): Query<HashMap<String, String>>,
    tcookies: Cookies,
) -> ServerQuery {
    
    // Get cookies into a HashMap.
    let mut cookies = HashMap::new();
    for cookie in tcookies.list() {
        let (name, value) = cookie.name_value();
        cookies.insert(name.to_string(), value.to_string());
    }

    // Build the ServerQuery.
    let mut sq = ServerQuery::new();
    sq.x.path = path;
    sq.x.query = params;
    sq.x.cookies = cookies;

    // Send query to database thread ( and get it back ).
    if sq.x.path != "/favicon.ico" {
        let mut state = state.lock().await;
        let _x = state.tx.send(sq).await;
        sq = state.rx.recv().await.unwrap()
    }

    sq
}

async fn my_post_handler(
    Extension(state): Extension<Arc<Mutex<SharedState>>>,
    Path(path): Path<String>,
    Query(params): Query<HashMap<String, String>>,
    form: Option<Form<HashMap<String, String>>>,
    tcookies: Cookies,
    mp: Option<Multipart>,
) -> ServerQuery {

    // Get the cookies into a HashMap.
    let mut cookies = HashMap::new();
    for cookie in tcookies.list() {
        let (name, value) = cookie.name_value();
        cookies.insert(name.to_string(), value.to_string());
    }

    // Get Vec of Parts.
    let mut parts = Vec::new();
    if let Some(mut mp) = mp {
        while let Some(field) = mp.next_field().await.unwrap() {
            let name = field.name().unwrap().to_string();
            let file_name = match field.file_name() {
                Some(s) => s.to_string(),
                None => "".to_string(),
            };
            let content_type = match field.content_type() {
                Some(s) => s.to_string(),
                None => "".to_string(),
            };
            let mut data = Vec::new();
            let mut text = "".to_string();
            if content_type.is_empty() {
                if let Ok(s) = field.text().await {
                    text = s;
                }
            } else if let Ok(bytes) = field.bytes().await {
                data = bytes.to_vec()
            }
            parts.push(Part {
                name,
                file_name,
                content_type,
                data,
                text,
            });
        }
    }

    // Build the ServerQuery.
    let mut sq = ServerQuery::new();
    sq.x.path = path;
    sq.x.query = params;
    sq.x.cookies = cookies;
    sq.x.parts = parts;
    if let Some(Form(form)) = form {
        sq.x.form = form;
    }

    // Send the ServerQuery to database thread ( and get it back ).
    let mut state = state.lock().await;
    let _x = state.tx.send(sq).await;
    state.rx.recv().await.unwrap()
}

use axum::{
    body::{Bytes, Full},
    http::header::HeaderName,
    http::status::StatusCode,
    http::{HeaderValue, Response},
    response::IntoResponse,
};

impl IntoResponse for ServerQuery {
    type Body = Full<Bytes>;
    type BodyError = std::convert::Infallible;

    fn into_response(self) -> Response<Self::Body> {
        let mut res = Response::new(Full::from(self.x.output));

        *res.status_mut() = StatusCode::from_u16(self.x.status_code).unwrap();

        for (name, value) in &self.x.headers {
            res.headers_mut().insert(
                HeaderName::from_lowercase(name.as_bytes()).unwrap(),
                HeaderValue::from_str(value).unwrap(),
            );
        }
        res
    }
}
4 Likes

What you are doing with the SharedState object here is a kind of actor, but the pattern where you are wrapping the channel pair in a mutex is not how it is usually implemented. Instead, you would typically only store a sender, and not a receiver. This eliminates the need for both the Arc and Mutex because the Sender itself can be cloned and behaves like an Arc. To receive responses, you would use an oneshot channel. See this article for more info.

5 Likes

Thanks! It's ok for the synchronous thread to send to the async oneshot channel then? Here's my program with your suggestion incorporated:

use mimalloc::MiMalloc;

/// Memory allocator ( MiMalloc ).
#[global_allocator]
static MEMALLOC: MiMalloc = MiMalloc;

use axum::{
    extract::{Extension, Form, Multipart, Path, Query},
    routing::get,
    AddExtensionLayer, Router,
};

use tower::ServiceBuilder;
use tower_cookies::{CookieManagerLayer, Cookies};

use tokio::sync::{mpsc, oneshot};

use database::{
    genquery::{GenQuery, Part},
    Database,
};

use std::{collections::HashMap, thread};

/// Query to be sent to server thread, implements IntoResponse.
struct ServerQuery {
    pub x: Box<GenQuery>,
}

impl ServerQuery {
    pub fn new() -> Self {
        Self {
            x: Box::new(GenQuery::new()),
        }
    }
}

/// Message to server thread, includes oneshot Sender for reply.
struct ServerMessage {
    pub sq: ServerQuery,
    pub tx: oneshot::Sender<ServerQuery>,
}

/// Main function ( execution starts here ).
#[tokio::main]
async fn main() {
    let (tx, mut rx) = mpsc::channel::<ServerMessage>(1);

    // This is the server thread (synchronous).
    thread::spawn(move || {
        let stg = Box::new(database::stg::SimpleFileStorage::new(
            "c:\\Users\\pc\\rust\\sftest01.rustdb",
        ));
        let db = Database::new(stg, database::init::INITSQL);
        loop {
            let mut sm = rx.blocking_recv().unwrap();
            db.run_timed("EXEC web.Main()", &mut *sm.sq.x);
            let _x = sm.tx.send(sm.sq);
            db.save();
        }
    });

    // build our application with a single route
    let app = Router::new().route("/*key", get(h_get).post(h_post)).layer(
        ServiceBuilder::new()
            .layer(CookieManagerLayer::new())
            .layer(AddExtensionLayer::new(tx)),
    );

    // run it with hyper on localhost:3000
    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

/// Get HashMap of cookies from Cookies.
fn map_cookies(cookies: Cookies) -> HashMap<String, String> {
    let mut result = HashMap::new();
    for cookie in cookies.list() {
        let (name, value) = cookie.name_value();
        result.insert(name.to_string(), value.to_string());
    }
    result
}

/// Get Vec of Parts from MultiPart.
async fn map_parts(mp: Option<Multipart>) -> Vec<Part> {
    let mut result = Vec::new();
    if let Some(mut mp) = mp {
        while let Some(field) = mp.next_field().await.unwrap() {
            let name = field.name().unwrap().to_string();
            let file_name = match field.file_name() {
                Some(s) => s.to_string(),
                None => "".to_string(),
            };
            let content_type = match field.content_type() {
                Some(s) => s.to_string(),
                None => "".to_string(),
            };
            let mut data = Vec::new();
            let mut text = "".to_string();
            if content_type.is_empty() {
                if let Ok(s) = field.text().await {
                    text = s;
                }
            } else if let Ok(bytes) = field.bytes().await {
                data = bytes.to_vec()
            }
            result.push(Part {
                name,
                file_name,
                content_type,
                data,
                text,
            });
        }
    }
    result
}

/// Handler for http GET requests.
async fn h_get(
    state: Extension<mpsc::Sender<ServerMessage>>,
    path: Path<String>,
    params: Query<HashMap<String, String>>,
    cookies: Cookies,
) -> ServerQuery {
    // Build the ServerQuery.
    let mut sq = ServerQuery::new();
    sq.x.path = path.0;
    sq.x.params = params.0;
    sq.x.cookies = map_cookies(cookies);

    // Send query to database thread ( and get it back ).
    let (tx, rx) = oneshot::channel::<ServerQuery>();
    let _err = state.send(ServerMessage { sq, tx }).await;
    rx.await.unwrap()
}

/// Handler for http POST requests.
async fn h_post(
    state: Extension<mpsc::Sender<ServerMessage>>,
    path: Path<String>,
    params: Query<HashMap<String, String>>,
    cookies: Cookies,
    form: Option<Form<HashMap<String, String>>>,
    multipart: Option<Multipart>,
) -> ServerQuery {
    // Build the ServerQuery.
    let mut sq = ServerQuery::new();
    sq.x.path = path.0;
    sq.x.params = params.0;
    sq.x.cookies = map_cookies(cookies);
    if let Some(Form(form)) = form {
        sq.x.form = form;
    } else {
        sq.x.parts = map_parts(multipart).await;
    }

    // Send query to database thread ( and get it back ).
    let (tx, rx) = oneshot::channel::<ServerQuery>();
    let _err = state.send(ServerMessage { sq, tx }).await;
    rx.await.unwrap()
}

use axum::{
    body::{Bytes, Full},
    http::{header::HeaderName, status::StatusCode, HeaderValue, Response},
    response::IntoResponse,
};

impl IntoResponse for ServerQuery {
    type Body = Full<Bytes>;
    type BodyError = std::convert::Infallible;

    fn into_response(self) -> Response<Self::Body> {
        let mut res = Response::new(Full::from(self.x.output));

        *res.status_mut() = StatusCode::from_u16(self.x.status_code).unwrap();

        for (name, value) in &self.x.headers {
            res.headers_mut().insert(
                HeaderName::from_lowercase(name.as_bytes()).unwrap(),
                HeaderValue::from_str(value).unwrap(),
            );
        }
        res
    }
}

Yes. The send method is not async.

1 Like

Right, and I guess of course that since it is one shot it can never block. The reason for my querying it was the mpsc documentation here which says:

When you want to communicate between synchronous and asynchronous code, there are two situations to consider:

Bounded channel : If you need a bounded channel, you should use a bounded Tokio mpsc channel for both directions of communication. Instead of calling the async send or recv methods, in synchronous code you will need to use the blocking_send or blocking_recv methods.

Perhaps it should mention the oneshot channel as well here? The documentation for sync, one level up, which I had not read, makes everything clear, but if you stumble on the mpsc documentation by itself you could believe ( as I did ) that the recipe given is the only way.

OT, but thanks for educating me about pattern matching on function params.

After 5 years of learning Rust, I can still discover something new!

Thanks for the feedback. I'll try to update that part of the docs.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.