use actix_web::{
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, Error, HttpRequest
};
use std::{
future::{ready, Ready as StdReady},
rc::Rc, error::Error as StdError,
};
use actix_session::Session;
use futures_util::future::LocalBoxFuture;
mod utils;
mod config;
pub use crate::config::IdempotentOptions;
use crate::utils::{bytes_to_response, hash_request, response_to_bytes};
pub struct IdempotentMiddleware<S> {
service: Rc<S>,
config: IdempotentOptions,
}
impl<S> Service<ServiceRequest> for IdempotentMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse, Error = Error> + 'static,
{
type Response = ServiceResponse;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
forward_ready!(service);
fn call(&self, mut req: ServiceRequest) -> Self::Future {
let srv = self.service.clone();
let config = self.config.clone();
Box::pin(async move {
let session = match req.extract::<Session>().await {
Ok(session) => session,
Err(err) => {
tracing::error!("Failed to extract Session from request: {:?}", err);
return srv.call(req).await;
}
};
let (req, hash) = hash_request(req, &config).await;
match check_cached_response(&hash, &session, req.request().clone()).await {
Ok(Some(res)) => return Ok(res),
Ok(None) => {} Err(err) => {
tracing::error!("Failed to check idempotent cached response: {:?}", err);
}
}
let res = srv.call(req).await?;
let (res, response_bytes) = response_to_bytes(res).await?;
if let Err(err) = session.insert(&hash, &response_bytes){
tracing::error!("Failed to cache idempotent response: {:?}", err);
}
Ok(res)
})
}
}
#[derive(Clone, Debug)]
pub struct IdempotentFactory {
config: IdempotentOptions,
}
impl IdempotentFactory {
pub const fn new(config: IdempotentOptions) -> Self {
IdempotentFactory {
config,
}
}
}
impl<S> Transform<S, ServiceRequest> for IdempotentFactory
where
S: Service<ServiceRequest, Response = ServiceResponse, Error = Error> + 'static,
S::Future: 'static,
{
type Response = ServiceResponse;
type Error = Error;
type InitError = ();
type Transform = IdempotentMiddleware<S>;
type Future = StdReady<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(IdempotentMiddleware {
service: Rc::new(service),
config: self.config.clone(),
}))
}
}
async fn check_cached_response(
hash: impl AsRef<str>,
session: &Session,
req: HttpRequest,
) -> Result<Option<ServiceResponse>, Box<dyn StdError + Send + Sync>> {
let response_bytes = session.get::<Vec<u8>>(hash.as_ref())?;
let res = if let Some(bytes) = response_bytes {
let response = bytes_to_response(bytes)?;
Some(ServiceResponse::new(req, response))
} else {
None
};
Ok(res)
}