Demystifying Actix Web Middleware

Written

For my Ergo task orchestrator side project, I’ve been writing the entire backend in Rust. As with any real project, this required writing some middleware, and while there are plenty of simple examples for Actix Web, it took a bit more effort to figure out what understand why things are done the way they are.

Javascript middleware often involves writing only a single function. The exact syntax differs between frameworks, but it usually looks something like this:

async function middleware(request, response, next) {
  try {
    let session = request.cookies['sid'];
    if(session) {
      req.user = await getUser(session);
    }
  } catch(e) {
    return next(e);
  }

  next();
}

app.use(middleware);

As with many things in Rust, middleware for the Actix Web framework is rather more complex. It requires implementing Transform and Service traits to get started, and you might also need an extractor that implements FromRequest to conveniently retrieve any extra data, such as the user object in the JS example above.

Actix provides a wrap_fn helper to make middleware with only a closure, similar to the JS example. This works for any middleware that can be written as a closure, but otherwise it’s not an option, so let’s continue so we can see what’s happening under the hood.

So we need a Transform, a Service, and maybe an extractor, but how do they fit together? For many middlewares, most of the complexity turns out to be boilerplate, but it still helps to understand what that boilerplate is actually doing. First, let’s take a step back and understand more generally what a Service represents.

The Service Trait 🔗

Actix’s Service represents anything that takes a request and returns a response. For HTTP, this includes both route handlers and middleware, but a Service doesn’t have to be specific to HTTP.

A middleware, then, is a special case of a Service that does some work and also calls another Service, which may be another layer of middleware or the endpoint handler.

Let’s take a look at the trait itself. The actual source file has plenty of comments which I’ve truncated here for brevity.

pub trait Service<Req> {
    /// Responses given by the service.
    type Response;

    /// Errors produced by the service when polling readiness or executing call.
    type Error;

    /// The future response value.
    type Future: Future<Output = Result<Self::Response, Self::Error>>;

    /// Returns `Ready` when the service is able to process requests.
    fn poll_ready(&self, ctx: &mut task::Context<'_>) -> Poll<Result<(), Self::Error>>;

    /// Process the request and return the response asynchronously.
    fn call(&self, req: Req) -> Self::Future;
}

In actix-web middleware, Response should always be a actix_web::dev::ServiceResponse and Error should be an actix_web::Error.

Actix calls poll_ready to determine if the service is ready to be invoked. This might be useful, for example, when the service needs limit the number of total times that it is called simultaneously. Usually you don’t need to provide your own implementation of this function, and actix-service version 2 provides the forward_ready! macro to delegate this function to the wrapped service.

The call function is where all the “real” functionality goes, and it isn’t too different from the JavaScript example. You can inspect or mutate the request and response objects as needed, and invoke the wrapped service if appropriate. There are three main differences from the JavaScript style:

  1. In most JS frameworks, the response object already exists and is passed into the middleware. Here, the response object is created by the wrapped service instead. The middleware can also create and return its own response object if appropriate.
  2. Errors are handled in normal Rust fashion, by returning a Result::Err instead of overloading the next function used to invoke the wrapped service.
  3. Since Rust is strongly typed, we can’t put extra data anywhere on the request. Instead, Actix has an “extension” mechanism to allow attaching extra data to the request for later retrieval. I’ll show an example of this later on.

The Transform Trait 🔗

Now that we some idea of how Service works, where does Transform come in? Abstractly, it “transforms” a service by wrapping it in another service, but in our case it’s easier to think of it as a factory. The Transform implementation’s only job is to create new middleware instances that wrap other services.

Transform trait object flow

Transform has some associated types, which mostly describe the same types on the created middleware Service. The only new type is InitError, which indicates an error that might occur when creating the middleware instance, if any.

pub trait Transform<S, Req> {
    /// Responses produced by the service.
    type Response;

    /// Errors produced by the service.
    type Error;

    /// The `TransformService` value created by this factory
    type Transform: Service<Req, Response = Self::Response, Error = Self::Error>;

    /// Errors produced while building a transform service.
    type InitError;

    /// The future response value.
    type Future: Future<Output = Result<Self::Transform, Self::InitError>>;

    /// Creates and returns a new Transform component, asynchronously
    fn new_transform(&self, service: S) -> Self::Future;
}

There’s a single function, new_transform, that creates a new instance of the middleware Service. The created middleware should wrap the service indicated by the service parameter.

new_transform returns a Future to allow some asynchronous work to be done while creating the middleware. We only need to create a new object, so we’ll use a Ready future type to wrap the new middleware inside a future. This is similar to using Javascript’s Promise.resolve to place a value inside a Promise.

Implementing the Middleware 🔗

For Ergo the first middleware I created is an authenticator which fetches user data, and so I’ll use that as the example here.

First, the middleware service structure. The middleware contains the service to be wrapped and an AuthData object, which has the logic for finding the user for each request.

pub type AuthenticationInfo = Rc<AuthenticationResult>;
pub struct AuthenticateMiddleware<S> {
    auth_data: Rc<AuthData>,
    service: Rc<S>,
}

AuthenticationInfo is a type alias that we’ll use later to make the information available to other parts of the app. We can use Rc instead of Arc here since Actix Web uses multiple single-thread runtimes and data won’t be sent between threads.

Next, the Service implementation for the middleware. The B type parameter here represents the type of the body returned from the service, which we pass into the type signature of ServiceResponse<B> but don’t otherwise care about in this case.

impl<S, B> Service<ServiceRequest> for AuthenticateMiddleware<S>
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;

    actix_service::forward_ready!(service);

This first part of the implementation will be pretty much the same for most middleware. We use the aforementioned forward_ready to delegate Service::poll_ready to the wrapped service.

The only other notable thing is the use of LocalBoxFuture for the Future type, which makes it easier to use an async block without needing to deal with the opaque future types returned by async blocks. LocalBoxFuture is the non-Send version of BoxFuture, in the same way that Rc can be used instead of Arc.

    fn call(&self, req: ServiceRequest) -> Self::Future {
	// Clone the Rc pointers so we can move them into the async block.
        let srv = self.service.clone();
        let auth_data = self.auth_data.clone();

        async move {
            // Get the session cookie value, if it exists. 
            let id = req.get_identity();
            // See if we can match it to a user.
            let auth = auth_data.authenticate(id, &req).await?;
            if let Some(auth) = auth {
                // If we found a user, add it to the request extensions
                // for later retrieval.
                req.extensions_mut()
                  .insert::<AuthenticationInfo>(Rc::new(auth));
            }

            let res = srv.call(req).await?;

            Ok(res)
        }
        .boxed_local()
    }
}

This block gets the session ID from the request cookies, managed by the actix-identity middleware, and passes it to AuthData::authenticate which gets the user information from the database. If we found the user, then we use the extensions API to insert it into the request. Actix uses the TypeId of the type parameter as the key here, so it will later be retrieved using the same AuthenticationInfo type.

In this case, we don’t throw an error on unauthenticated requests. Later code can decide if it requires a user or not.

Next, we have the factory object which implements Transform.

pub struct AuthenticateMiddlewareFactory {
    auth_data: Rc<AuthData>,
}

impl AuthenticateMiddlewareFactory {
    pub fn new(auth_data: AuthData) -> Self {
        AuthenticateMiddlewareFactory {
            auth_data: Rc::new(auth_data),
        }
    }
}

impl<S, B> Transform<S, ServiceRequest> for AuthenticateMiddlewareFactory
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type Transform = AuthenticateMiddleware<S>;
    type InitError = ();
    type Future = Ready<Result<Self::Transform, Self::InitError>>;

    fn new_transform(&self, service: S) -> Self::Future {
        ready(Ok(AuthenticateMiddleware {
            auth_data: self.auth_data.clone(),
            service: Rc::new(service),
        }))
    }
}

As discussed, the types mostly match those used for the middleware service type, and new_transform creates an instance of the middleware and uses ready to wrap it in a future.

Finally, we can add it to the server using wrap.

let authdata = AuthData::new(...);
let identity = actix_identity::IdentityService::new(...);

App::new().service(
  web::scope("/api")
    .wrap(AuthenticateMiddlewareFactory::new(
      authdata.clone()
    ))
    .wrap(identity)
    .wrap(TracingLogger::default())
    .configure(web_app_server::config)
    .configure(tasks::handlers::config)
    .configure(status_server::config)
  )

Retrieving the User Information 🔗

Now that the middleware is installed, we can use it in a route handler, another middleware, or a request extractor.

The first two options involve using the request extensions API to look up the object with the correct type: req.extensions().get::<AuthenticationInfo>(). This uses the same TypeId key that we used when adding the information from the middleware.

Actix also makes heavy use of request extractors. Route handlers commonly use Data, Path, Json, or similar types to expose information from the request, and these types all implement the FromRequest trait. We can make our own implementation to ease the process of extracting the AuthenticationInfo.

pub struct Authenticated(AuthenticationInfo);

impl FromRequest for Authenticated {
    type Error = Error;
    type Future = Ready<Result<Self, Self::Error>>;

    fn from_request(req: &actix_web::HttpRequest,
            payload: &mut actix_web::dev::Payload) -> Self::Future {

        let value = req.extensions().get::<AuthenticationInfo>().cloned();
        let result = match value {
            Some(v) => Ok(Authenticated(v)),
            None => Err(Error::AuthenticationError),
        };
        ready(result)
    }
}

impl std::ops::Deref for Authenticated {
    type Target = AuthenticationInfo;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

This function also uses the extensions API to get the user information, and then returns it inside a Ready future. We also implement Deref to make it easy to use the AuthenticationInfo object embedded in the Authenticated extractor.

Now that we have our extractor, we can drop it into the argument list of any handler.

#[put("/tasks/{task_id}")]
async fn write_task_handler(
    task_id: Path<String>,
    app_data: AppStateData,
    payload: Json<SomeObject>,
    req: HttpRequest,
    auth: Authenticated,
) -> Result<impl Responder> {
    let org = auth.org_id();
    let user = auth.user_id();
    todo!();
}

A nice bonus here is that the request extractor both retrieves the authentication information and also verifies that a user was actually found. Its very presence in the function signature will make unauthenticated requests to that endpoint fail with an appropriate error.

I’ve also implemented a MaybeAuthenticated extractor, which works similarly but always succeeds and returns an Option<AuthenticationInfo>. This enables handlers that may customize their behavior for logged-in users, but also work with anonymous users.

pub struct MaybeAuthenticated(Option<AuthenticationInfo>);

impl FromRequest for MaybeAuthenticated {
    // ... all the same associated types go here
    fn from_request(req: &HttpRequest,
            _payload: &mut actix_web::dev::Payload) -> Self::Future {
        let value = req.extensions().get::<AuthenticationInfo>().cloned();
        ready(Ok(MaybeAuthenticated(value)))
    }
}

When writing this code, I found that the difficult part was understanding the purpose of the boilerplate and what it was doing, but actually writing the purpose-specific parts of the middleware wasn’t too complex in the end. Hopefully this makes things easier to understand!


Thanks for reading! If you have any questions or comments, please send me a note on Twitter.