DEV Community

Dustin Speckhals
Dustin Speckhals

Posted on

Upgrading to Rust's Actix Web 2.0

For the past year or more, I've been passively watching the progress of the "async/await" implementation in the Rust programming language. Though I can't speak to the technical details in the Rust compiler, it's been neat to learn new concepts (to me) like Pin, waker, and Future; these aren't things I deal with often as a higher-level developer.

A Little Context

In September, 2018 I took a little adventure developing a small web application built with the actix-web framework: Bible.rs. This app is relatively simple, but I tried to use actix-web to its most powerful extent. You can read more about the initial development of Bible.rs in this Reddit post.

A few weeks before I wrote this post, async/await landed on Rust stable. Since the syntax for this new functionality stabilized in the springtime, I know that Rust async crate maintainers had been working on migrating to std::future, most notable Tokio. When Tokio published an alpha version with the std::future::Future and async/await, discussion and work began on migrating actix-web as well. I watched closely as the main actix maintainer migrated to async/await (which was a ton of work in actix-net and actix-web). A few days ago, an alpha version was released, and I got to work on upgrading Bible.rs! This post is my experience upgrading to actix-web 2.0. The actix-web API may change in the alpha and beta process, and if it does, I'll be sure to update accordingly.

Upgrading actix-web

The first step I took after opening up my editor was to change the Cargo.toml entries.

  • actix-files (middleware for static files): 0.1.4 -> 0.2.0-alpha.1
  • actix-web: 1.0.5 -> 2.0.0-alpha.1
  • futures: 0.1.28 -> 0.3.1

Of course, this caused a decent amount of compiler errors, but not as many as I anticipated. The first breaking change I dealt with had to do with the registering of handler functions on a route. Before 2.0, there were two functions that could be used for handler registration: to and to_async. to just had to return a type that could be cast to a response, where to_async had to return a Future. In 2.0, all handlers return a Future (now from std::future instead of the futures crate). After renaming all of the handler registrations from to_async to just to, the compiler let me know that the handlers were now returning the wrong type.

Converting a Handler to Async/Await

I've been using async accessing of the database through a thread pool that actix-web provides in web::block for some time. This might be a little bit of an overkill, but it's been a good learning experience understanding thread pools and blocking code.

Before: actix-web 1.0

pub fn reference<SD>(
    data: web::Data<ServerData>,
    path: web::Path<(String,)>,
    req: HttpRequest,
) -> impl Future<Item = HttpResponse, Error = HtmlError>
where
    SD: SwordDrillable,
{
    let db = data.db.to_owned();
    let raw_reference = path.0.replace("/", ".");
    match raw_reference.parse::<Reference>() {
        Ok(reference) => {
            let data_reference = reference.to_owned();
            Either::A(
                web::block(move || SD::verses(&reference, &VerseFormat::HTML, &db.get().unwrap()))
                    .map_err(HtmlError::from)
                    .and_then(move |result| {
                        let verses_data = VersesData::new(result, data_reference, &req);

                        if verses_data.verses.is_empty() {
                            return Err(Error::InvalidReference {
                                reference: raw_reference,
                            }
                            .into());
                        }

                        let body = TemplateData::new(
                            &verses_data,
                            Meta::for_reference(
                                &verses_data.reference,
                                &verses_data.verses,
                                &verses_data.links,
                            ),
                        )
                        .to_html("chapter", &data.template)?;
                        Ok(HttpResponse::Ok().content_type("text/html").body(body))
                    }),
            )
        }
        Err(_) => Either::B(err(HtmlError(Error::InvalidReference {
            reference: raw_reference,
        }))),
    }
}

There are a few things in this function that had to be done in order to make the compiler happy before std::future and async/await. First, it had to return an impl Trait type for to_async. You'll also notice that if the inputted path variable is not "valid" it skips the database call (and web::block completely). A Future still has to be returned though, which is why the response is wrapped in err (similar to reject for a JavaScript Promise). However, this wasn't quite good enough for the handler's return type yet; both the happy-path and error results still have to be wrapped in Either, thus combining the two possible results into a "single" return type. Yes, all of this works (and did fine for a year), but it was a major process of trial and error even getting to this point.

You'll observe that the truly async portion of this handler is the bit of code around web::block. In order to make this work, you have write the "what do I do with the result after this finishes" code in and_then. Rightward drift of the code across your screen can happen very easily. Now imagine having two or more consecutive async processes.

After: actix-web 2.0

Before I start on the explanation of the migration, I realized after everything compiled and tests passed that this is quite similar to moving a JavaScript Promise to its own async/await syntax.

pub async fn reference<SD>(
    data: web::Data<ServerData>,
    path: web::Path<(String,)>,
    req: HttpRequest,
) -> Result<HttpResponse, HtmlError>
where
    SD: SwordDrillable,
{
    let db = data.db.to_owned();
    let raw_reference = path.0.replace("/", ".");
    match raw_reference.parse::<Reference>() {
        Ok(reference) => {
            let data_reference = reference.to_owned();
            let result =
                web::block(move || SD::verses(&reference, &VerseFormat::HTML, &db.get().unwrap()))
                    .await??;
            let verses_data = VersesData::new(result, data_reference, &req);

            if verses_data.verses.is_empty() {
                return Err(Error::InvalidReference {
                    reference: raw_reference,
                }
                .into());
            }

            let body = TemplateData::new(
                &verses_data,
                Meta::for_reference(
                    &verses_data.reference,
                    &verses_data.verses,
                    &verses_data.links,
                ),
            )
            .to_html("chapter", &data.template)?;
            Ok(HttpResponse::Ok().content_type("text/html").body(body))
        }
        Err(_) => Err(HtmlError(Error::InvalidReference {
            reference: raw_reference,
        })),
    }
}

The first thing of note is adding the new (as of Rust 1.39) keyword async to the function signature. This lets the compiler know that this function can be awaited (or less likely, polled). To put it very simply, the return type from the function is "automatically" wrapped in a Future. With this specific function, it takes away the need of returning an impl Future type. Instead, it can return anything that can be a Future::Output; in this case a Result<HttpResponse, HtmlError>. With this change to the signature, this function is now the correct actix-web handler type for to(handler)! But it gets better.

The code in the and_then closure for web::block can now be moved out and into the main execution path. You can then use the new await keyword at the end of web::block instead of and_then. Note the two question mark operators (.await??); the first unpacks the result of the actual Future (e.g., it might be cancelled), while the second unpacks the result of the database call. Perhaps I could eliminate the need for two result unpacks, but it works for now. Of course, now that the result handling code is after await, the entire and_then call can be removed. I found that map_err was no longer necessary to make the compiler happy either.

You'll notice that the wrapping the error match for the input validation no longer has to be wrapped in err, and Either isn't necessary either. I think this handler function is much simpler now!

Here's the PR for the upgrade: https://github.com/DSpeckhals/bible.rs/pull/44

Conclusion

I'm extremely pleased with the result and migration for this simple application. Async I/O can lend itself to confusing code in any programming language, and in Rust with its strong type system, it can bring a steep learning curve along with it. With the addition of std::future and async/await, I think that async programming is much more accessible and ergonomic; especially in actix-web. I'm impressed with the work that the Rust language team and working groups accomplished. The fairly quick turnaround to get actix-web to this point was also impressive (thanks to fafhrd91 and a few others). I look forward to seeing what's next in Rust's async story and actix-web.

Top comments (0)