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.
Rust: learning actix-web middleware 01. |
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:
- Module actix_web::middleware -- official documentation on middleware.
- Demystifying Actix Web Middleware -- I find the author explains middleware in a “programmer context”, greatly complements the official documentation.
- GitHub actix examples middleware -- I don't know if this's the official repo or not, they seem to demonstrate a lot of middleware capabilities.
- actix GitHub middleware request-extensions example -- this example demonstrates how to attach custom data to request extension in a middleware, and having an endpoint handler to extract and return this custom data to the requesting client.
- Rust language user forum | How to pass data to an actix middleware -- discussions and a solution on how a middleware can access application state data.
❶ 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
:
-
The import line:
use actix_web::{web, App, HttpServer};
-
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!("<h1>{}</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:
- https://www.omgubuntu.co.uk/2022/09/ubuntu-2210-kinetic-kudu-default-wallpaper
- https://in.pinterest.com/pin/337277459600111737/
- https://www.rust-lang.org/
- https://www.pngitem.com/download/ibmJoR_rust-language-hd-png-download/