Rust: learning actix-web middleware 01.

We add request path extractor and MySQL database query calls to the official SayHi middleware example. The middleware creates a text message, attaches it to the request via extension. This text message contains the detail of the first matched record, if any found, otherwise a no record found message. A resource endpoint service handler then extracts this message, and returns it as HTML to the client.

🦀 Index of the Complete Series.

We’ve previously discussed a simple web application in Rust web application: MySQL server, sqlx, actix-web and tera. We’re going to add this refactored SayHi middleware to this existing web application.

🚀 Please note: complete code for this post can be downloaded from GitHub with:

git clone -b v0.2.0 https://github.com/behai-nguyen/rust_web_01.git

Following are documentation, posts and examples which help me to write the example code in this post:

❶ To start off, we’ll get the SayHi middleware to run as an independent web project. I’m reprinting the example code with a complete fn main().

Cargo.toml dependencies section is as follow:

...
[dependencies]
actix-web = "4.4.0"
Content of src/main.rs:
use std::{future::{ready, Ready, Future}, pin::Pin};

use actix_web::{
    dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
    Error,
};

use actix_web::{web, App, HttpServer};

pub struct SayHi;

// `S` - type of the next service
// `B` - type of response's body
impl<S, B> Transform<S, ServiceRequest> for SayHi
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type InitError = ();
    type Transform = SayHiMiddleware<S>;
    type Future = Ready<Result<Self::Transform, Self::InitError>>;

    fn new_transform(&self, service: S) -> Self::Future {
        ready(Ok(SayHiMiddleware { service }))
    }
}

pub struct SayHiMiddleware<S> {
    /// The next service to call
    service: S,
}

// This future doesn't have the requirement of being `Send`.
// See: futures_util::future::LocalBoxFuture
type LocalBoxFuture<T> = Pin<Box<dyn Future<Output = T> + 'static>>;

// `S`: type of the wrapped service
// `B`: type of the body - try to be generic over the body where possible
impl<S, B> Service<ServiceRequest> for SayHiMiddleware<S>
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type Future = LocalBoxFuture<Result<Self::Response, Self::Error>>;

    // This service is ready when its next service is ready
    forward_ready!(service);

    fn call(&self, req: ServiceRequest) -> Self::Future {
        println!("Hi from start. You requested: {}", req.path());

        // A more complex middleware, could return an error or an early response here.

        let fut = self.service.call(req);

        Box::pin(async move {
            let res = fut.await?;

            println!("Hi from response");
            Ok(res)
        })
    }
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .wrap(SayHi)
            .route("/", web::get().to(|| async { "Hello, middleware!" }))
    })
    .bind(("0.0.0.0", 5000))?
    .run()
    .await      
}

Please note: all examples have been tested on both Windows 10 and Ubuntu 22.10.

192.168.0.16 is the address of my Ubuntu 22.10 machine, I run tests from my Windows 10 Pro machine.

Run http://192.168.0.16:5000/, we see the following response on the browser and screen:

We can see the flow of the execution path within the different parts of the middleware.

(I’m using the same project to test completely different pieces of code, by replacing the content of main.rs and the [dependencies] section of Cargo.toml, that is why the target executable is learn-actix-web.)

❷ Integrate SayHi into Rust web application: MySQL server, sqlx, actix-web and tera.

🚀 We’re just showing new code snippets added throughout the discussions. The complete source code for this post is on GitHub. It’s been tagged with v0.2.0. To download, run the command:

git clone -b v0.2.0 https://github.com/behai-nguyen/rust_web_01.git

The code also has a fair amount of documentation which hopefully makes the reading a bit easier.

⓵ Copy the entire src/main.rs in the above example to this project src/middleware.rs, then remove the following from the new src/middleware.rs:

  1. The import line: use actix_web::{web, App, HttpServer};
  2. The entire fn main(), starting from the line #[actix_web::main].

The project directory now looks like in the screenshot below:

Module src/middleware.rs now contains the stand-alone SayHi middleware as per in the official documentation. It is ready for integration into any web project.

⓶ Initially, apply SayHi middleware as is to a new application resource http://0.0.0.0:5000/helloemployee.

In src/main.rs, we need to include src/middleware.rs module, and create another service which wraps both resource /helloemployee and SayHi middleware.

Updated content of src/main.rs:
...
mod handlers;

mod middleware;

pub struct AppState {
...

            .service(
                web::scope("/ui")
                    .service(handlers::employees_html1)
                    .service(handlers::employees_html2),
            )
            .service(
                web::resource("/helloemployee")
                    .wrap(middleware::SayHi)
                    .route(web::get().to(|| async { "Hello, middleware!" }))
            )
...

It should compile and run successfully. All existing four (4) routes should operate as before:

🚀 And they should not trigger the SayHi middleware!

The new route, http://0.0.0.0:5000/helloemployee should run as per the above example:

⓷ As has been outlined above, all I’d like to do in this learning exercise is to get the middleware to do request path extractions, database query, and return some text message to the middleware endpoint service handler. Accordingly, the resource route changed to /helloemployee/{last_name}/{first_name}.

Updated content of src/main.rs:
...
            .service(
                web::resource("/helloemployee/{last_name}/{first_name}")
                    .wrap(middleware::SayHi)
                    .route(web::get().to(|| async { "Hello, middleware!" }))
            )
...

In the middleware, fn call(&self, req: ServiceRequest) -> Self::Future has new code to extract last_name and first_name from the path, and print them out to stdout:

Updated content of src/middleware.rs:
...
    fn call(&self, req: ServiceRequest) -> Self::Future {
        println!("Hi from start. You requested: {}", req.path());

        let last_name: String = req.match_info().get("last_name").unwrap().parse::<String>().unwrap();
        let first_name: String = req.match_info().get("first_name").unwrap().parse::<String>().unwrap();

        println!("Middleware. last name: {}, first name: {}.", last_name, first_name);

        // A more complex middleware, could return an error or an early response here.

        let fut = self.service.call(req);

        Box::pin(async move {
            let res = fut.await?;

            println!("Hi from response");
            Ok(res)
        })
    }
...

When we run the updated route http://192.168.0.16:5000/helloemployee/%chi/%ak, the output on the browser should stay the same. The output on the screen changes to:

Hi from start. You requested: /helloemployee/%chi/%ak
Middleware. last name: %chi, first name: %ak.
Hi from response

⓸ The next step is to query the database using the extracted partial last name and partial first name. For that, we need to get access to the application state which has the database connection pool attached. This Rust language user forum post has a complete solution 😂 Accordingly, the middleware code is updated as follows:

Updated content of src/middleware.rs:
...
use actix_web::{
    dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
    // New import web::Data.
    web::Data, Error,
};

use async_std::task;

use super::AppState;
use crate::models::get_employees;
...

    fn call(&self, req: ServiceRequest) -> Self::Future {
        ...
        println!("Middleware. last name: {}, first name: {}.", last_name, first_name);

        // Retrieve the application state, where the database connection object is.
        let app_state = req.app_data::<Data<AppState>>().cloned().unwrap();
        // Query the database using the partial last name and partial first name.
        let query_result = task::block_on(get_employees(&app_state.db, &last_name, &first_name));
        ...

Not to bore you with so many intermediate debug steps, I just show the final code. But to get to this, I did do debug print out over several iterations to verify I get what I expected to get, etc. I also print out content of query_result to assert that I get the records I expected. (I should learn the Rust debugger!)

⓹ For the final step, which is getting the middleware to attach a custom text message to the request, and then the middleware endpoint service handler extracts this message, process it further, before sending it back the requesting client.

The principle reference for this part of the code is the actix GitHub middleware request-extensions example. The middleware needs to formulate the message and attach it to the request via the extension. The final update to src/middleware.rs:

Updated content of src/middleware.rs:
...
use actix_web::{
    dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
    // New import HttpMessage.
    web::Data, Error, HttpMessage,
};
...
#[derive(Debug, Clone)]
pub struct Msg(pub String);
...
    fn call(&self, req: ServiceRequest) -> Self::Future {
        ...
        // Attached message to request.
        req.extensions_mut().insert(Msg(hello_msg.to_owned()));
        ...

For the endpoint service handler, we add a new function hi_first_employee_found to src/handlers.rs:

Updated content of src/handlers.rs:
...
// New import web::ReqData.
use actix_web::{get, post, web, HttpRequest, HttpResponse, Responder, web::ReqData};
...
use crate::middleware::Msg;
...
pub async fn hi_first_employee_found(msg: Option<ReqData<Msg>>) -> impl Responder {
    match msg {
        None => return HttpResponse::InternalServerError().body("No message found."),

        Some(msg_data) => {
            let Msg(message) = msg_data.into_inner();

            HttpResponse::Ok()
                .content_type("text/html; charset=utf-8")
                .body(format!("&lt;h1>{}&lt;/h1>", message))    
        },
    }
}

To make it a bit “dynamic”, if the request extension Msg is found, we wrap it in an HTML h1 tag, and return the response as HTML. Otherwise, we just return an HTTP 500 error code. But given the code as it is, the only way to trigger this error is to comment out the req.extensions_mut().insert(Msg(hello_msg.to_owned())); in the middleware fn call(&self, req: ServiceRequest) -> Self::Future above.

The last step is to make function hi_first_employee_found the middleware endpoint service handler.

Updated content of src/main.rs:
...
            .service(
                web::resource("/helloemployee/{last_name}/{first_name}")
                    .wrap(middleware::SayHi)
                    .route(web::get().to(handlers::hi_first_employee_found))
            )
...

The route http://192.168.0.16:5000/helloemployee/%chi/%ak should now respond:

When no employee found, e.g., route http://192.168.0.16:5000/helloemployee/%xxx/%yyy, the response is:

This middleware does not do much, but it’s a good learning curve for me. I hope you find this post helpful somehow. Thank you for reading and stay safe as always.

✿✿✿

Feature image source:

🦀 Index of the Complete Series.

Design a site like this with WordPress.com
Get started