rustcache-controlactix-web

How to set "expire" or "Cache-Control" header when serving static files with actix-files?


I want to set a cache TTL for static files using actix_files.

Like in Nginx config: expires max; will add such header: expires: Thu, 31 Dec 2037 23:55:55 GMT.

How can I do it with actix_files?

use actix_files::Files;
use actix_web::{App, HttpServer, web, HttpResponse, http, cookie, middleware};

#[actix_web::main]
async fn main() {
    HttpServer::new(move || {
        App::new()
            .wrap(middleware::Logger::default())
            .wrap(middleware::Compress::default())
            .service(Files::new("/dist", "dist/"))
    })
        .bind("0.0.0.0:8080").unwrap()
        .run()
        .await.unwrap();
}

Solution

  • My suggested approach is through middleware. This code could be made less verbose using .wrap_fn.

    use actix_files::Files;
    use actix_service::{Service, Transform};
    use actix_web::{
        dev::ServiceRequest,
        dev::ServiceResponse,
        http::header::{HeaderValue, EXPIRES},
        middleware, web, App, Error, HttpServer,
    };
    // use actix_http::http::header::Expires;
    use futures::{
        future::{ok, Ready},
        Future,
    };
    
    use std::pin::Pin;
    use std::task::{Context, Poll};
    
    struct MyCacheInterceptor;
    
    impl<S, B> Transform<S> for MyCacheInterceptor
    where
        S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
        S::Future: 'static,
        B: 'static,
    {
        type Request = ServiceRequest;
        type Response = ServiceResponse<B>;
        type Error = Error;
        type InitError = ();
        type Transform = MyCacheInterceptorMiddleware<S>;
        type Future = Ready<Result<Self::Transform, Self::InitError>>;
    
        fn new_transform(&self, service: S) -> Self::Future {
            ok(MyCacheInterceptorMiddleware { service })
        }
    }
    
    pub struct MyCacheInterceptorMiddleware<S> {
        service: S,
    }
    
    impl<S, B> Service for MyCacheInterceptorMiddleware<S>
    where
        S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
        S::Future: 'static,
        B: 'static,
    {
        type Request = ServiceRequest;
        type Response = ServiceResponse<B>;
        type Error = Error;
        type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
    
        fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
            self.service.poll_ready(cx)
        }
    
        fn call(&mut self, req: ServiceRequest) -> Self::Future {
            let fut = self.service.call(req);
    
            Box::pin(async move {
                let mut res = fut.await?;
                let headers = res.headers_mut();
                headers.append(
                    EXPIRES,
                    HeaderValue::from_static("Thu, 31 Dec 2037 23:55:55 GMT"),
                );
                return Ok(res);
            })
        }
    }
    
    #[actix_web::main]
    async fn main() {
        HttpServer::new(move || {
            App::new()
                .wrap(middleware::Logger::default())
                .wrap(middleware::Compress::default())
                .service(
                    web::scope("/dist")
                        .wrap(MyCacheInterceptor)
                        .service(Files::new("", ".").show_files_listing()),
                )
        })
        .bind("0.0.0.0:8080")
        .unwrap()
        .run()
        .await
        .unwrap();
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        use actix_web::{test, web, App};
    
        #[actix_rt::test]
        async fn test_expire_header() {
            let mut app = test::init_service(
                App::new().service(
                    web::scope("/")
                        .wrap(MyCacheInterceptor)
                        .service(Files::new("", ".").show_files_listing()),
                ),
            )
            .await;
            let req = test::TestRequest::with_header("content-type", "text/plain").to_request();
            let resp = test::call_service(&mut app, req).await;
            assert!(resp.status().is_success());
            assert!(resp.headers().get(EXPIRES).is_some());
            assert_eq!(
                resp.headers().get(EXPIRES).unwrap(),
                HeaderValue::from_static("Thu, 31 Dec 2037 23:55:55 GMT"),
            );
        }
    }