In the tenth post of our actix-web learning application, we added an ad hoc middleware. In this post, with the assistance of the actix-web-lab crate, we will refactor this ad hoc middleware into a standalone async function to enhance the overall code readability.

πŸ¦€ Index of the Complete Series.

103-feature-image.png
Rust: Actix-web – Async Functions as Middlewares

πŸš€ Please note, complete code for this post can be downloaded from GitHub with:

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

The actix-web learning application mentioned above has been discussed in the twelve previous posts. The index of the complete series can be found here.

The code we’re developing in this post is a continuation of the code from the twelfth post. πŸš€ To get the code of this twelfth post, please use the following command:

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

– Note the tag v0.12.0.

While this post continues from previous posts in this series, it can be read in conjunction with only the tenth post, focusing particularly on the section titled Code Updated in the src/lib.rs Module.

❢ For this post, no new modules are introduced. Instead, we will update existing modules and files. The layout chart below displays the updated files and modules, with those marked with β˜… indicating the ones that have been updated.

.
β”œβ”€β”€ Cargo.toml β˜…
β”œβ”€β”€ ...
β”œβ”€β”€ README.md β˜…
└── src
  β”œβ”€β”€ lib.rs β˜…
  └── ...

❷ An update to the Cargo.toml file:

...
[dependencies]
...
actix-web-lab = "0.20.2"

We added the new crate actix-web-lab. This crate is:

In-progress extractors and middleware for Actix Web.

This crate provides mechanisms for implementing middlewares as standalone async functions, rather than using actix-web’s wrap_fn.

According to the documentation, the actix-web-lab crate is essentially experimental. Functionalities implemented in this crate might eventually be integrated into the actix-web crate. In such a case, we would need to update our code.

❸ Refactor an existing ad hoc middleware out of wrap_fn.

As mentioned at the beginning, this post should be read in conjunction with the tenth post, where we introduced this ad hoc middleware. The description of this simple middleware functionality is found in the section Code Updated in the src/lib.rs Module of the tenth post.

Below, we reprint the code of this ad hoc middleware:

            //
            // This ad hoc middleware looks for the updated access token String attachment in 
            // the request extension, if there is one, extracts it and sends it to the client 
            // via both the ``authorization`` header and cookie.
            //
            .wrap_fn(|req, srv| {
                let mut updated_access_token: Option<String> = None;

                // Get set in src/auth_middleware.rs's 
                // fn update_and_set_updated_token(request: &ServiceRequest, token_status: TokenStatus).
                if let Some(token) = req.extensions_mut().get::<String>() {
                    updated_access_token = Some(token.to_string());
                }

                srv.call(req).map(move |mut res| {

                    if updated_access_token.is_some() {
                        let token = updated_access_token.unwrap();
                        res.as_mut().unwrap().headers_mut().append(
                            header::AUTHORIZATION, 
                            header::HeaderValue::from_str(token.as_str()).expect(TOKEN_STR_JWT_MSG)
                        );

                        let _ = res.as_mut().unwrap().response_mut().add_cookie(
                            &build_authorization_cookie(&token));
                    };

                    res
                })
            })

It’s not particularly lengthy, but its inclusion in the application instance construction process makes it difficult to read. While closures can call functions, refactoring this implementation into a standalone function isn’t feasible. This is because the function would require access to the parameter srv, which in this case refers to the AppRouting struct. Please refer to the screenshot below for clarification:


The AppRouting struct is located in the private module actix-web/src/app_service.rs, which means we don’t have direct access to it. I attempted to refactor it into a standalone function but encountered difficulties. Someone else had also attempted it before me and faced similar issues.

Please refer to the GitHub issue titled wrap_fn &AppRouting should use Arc<AppRouting> #2681 for more details. This reply suggests using the actix-web-lab crate.

I believe I’ve come across this crate before, particularly the function actix_web_lab::middleware::from_fn, but it didn’t register with me at the time.

Drawing from the official example actix-web-lab/actix-web-lab/examples/from_fn.rs and compiler suggestions, I’ve successfully refactored the ad hoc middleware mentioned above into the standalone async function async fn update_return_jwt<B>(req: ServiceRequest, next: Next<B>) -> Result<ServiceResponse<B>, Error>. The screenshot below, taken from Visual Studio Code with the Rust-Analyzer plugin, displays the full source code and variable types:


Compared to the original ad hoc middleware, the code is virtually unchanged. It’s worth noting that this final version is the result of my sixth or seventh attempt; without the compiler suggestions, I would not have been able to complete it. We register it with the application instance using only a single line, as per the documentation:

            .wrap(from_fn(update_return_jwt))

❹ Other minor refactorings include optimising the application instance builder code for brevity. Specifically, I’ve moved the code to create the CORS instance to the standalone function fn cors_config(config: &config::Config) -> Cors, and the code to create the session store to the standalone async function async fn config_session_store() -> (actix_web::cookie::Key, RedisSessionStore).

Currently, the src/lib.rs module is less than 250 lines long, housing 7 helper functions that are completely unrelated. I find it still very manageable. The code responsible for creating the server instance and the application instance, encapsulated in the function pub async fn run(listener: TcpListener) -> Result<Server, std::io::Error>, remains around 60 lines. Although I anticipate it will grow a bit more as we add more functionalities, I don’t foresee it becoming overly lengthy.

❺ I am happy to have learned something new about actix-web. And I hope you find the information useful. Thank you for reading. And stay safe, as always.

✿✿✿

Feature image source:

πŸ¦€ Index of the Complete Series.