rari 0.13.2

rari: Runtime Accelerated Rendering Infrastructure - A high-performance React Server Components framework powered by Rust
Documentation
mod cache;
mod config;
mod optimizer;
mod types;

pub use config::{ImageConfig, ImageVariant, LocalPattern, RemotePattern};
pub use optimizer::{ImageOptimizer, PreloadImage};
pub use types::{ImageFormat, OptimizeParams, OptimizedImage};

use axum::{
    extract::{ConnectInfo, Query, State},
    http::{HeaderMap, StatusCode, header},
    response::{IntoResponse, Response},
};
use std::net::SocketAddr;
use std::sync::Arc;

use crate::server::security::ip_rate_limiter::EndpointRateLimiters;
use crate::server::utils::ip_extractor::extract_client_ip;

#[derive(Clone)]
pub struct ImageState {
    pub optimizer: Arc<ImageOptimizer>,
    pub rate_limiters: EndpointRateLimiters,
}

pub async fn handle_image_request(
    State(state): State<ImageState>,
    ConnectInfo(addr): ConnectInfo<SocketAddr>,
    headers: HeaderMap,
    Query(params): Query<OptimizeParams>,
) -> Result<Response, ImageError> {
    let ip = extract_client_ip(&headers, &addr);
    if let Err(retry_after) = state.rate_limiters.image_optimization.check(&ip) {
        return Ok((
            StatusCode::TOO_MANY_REQUESTS,
            [
                (header::RETRY_AFTER, retry_after.to_string()),
                (header::CONTENT_TYPE, "text/plain".to_string()),
            ],
            "Rate limit exceeded for image optimization. Please try again later.",
        )
            .into_response());
    }

    let (optimized, cache_hit) = state.optimizer.optimize(params).await?;

    let content_type = match optimized.format {
        ImageFormat::Avif => "image/avif",
        ImageFormat::WebP => "image/webp",
        ImageFormat::Jpeg => "image/jpeg",
        ImageFormat::Png => "image/png",
        ImageFormat::Gif => "image/gif",
    };

    let is_production = std::env::var("NODE_ENV").map(|v| v == "production").unwrap_or(false);

    let cache_header = if is_production {
        "public, max-age=31536000, immutable"
    } else {
        "public, max-age=0, must-revalidate"
    };

    let x_cache = if cache_hit { "HIT" } else { "MISS" };

    let mut response = (
        StatusCode::OK,
        [(header::CONTENT_TYPE, content_type), (header::CACHE_CONTROL, cache_header)],
        optimized.data,
    )
        .into_response();

    response
        .headers_mut()
        .insert("x-cache", x_cache.parse().expect("x-cache header value should be valid ASCII"));

    Ok(response)
}

#[derive(Debug, thiserror::Error)]
pub enum ImageError {
    #[error("Invalid URL: {0}")]
    InvalidUrl(String),
    #[error("Unauthorized domain: {0}")]
    UnauthorizedDomain(String),
    #[error("Failed to fetch image: {0}")]
    FetchError(String),
    #[error("Failed to process image: {0}")]
    ProcessingError(String),
    #[error("Invalid parameters: {0}")]
    InvalidParams(String),
}

impl IntoResponse for ImageError {
    fn into_response(self) -> Response {
        let (status, message) = match self {
            ImageError::InvalidUrl(_) | ImageError::InvalidParams(_) => {
                (StatusCode::BAD_REQUEST, self.to_string())
            }
            ImageError::UnauthorizedDomain(_) => (StatusCode::FORBIDDEN, self.to_string()),
            ImageError::FetchError(_) => (StatusCode::BAD_GATEWAY, self.to_string()),
            ImageError::ProcessingError(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
        };

        (status, message).into_response()
    }
}