use ::axum::{
Router,
body::Body,
extract::State,
http::{HeaderMap, StatusCode, Uri, header},
response::{IntoResponse, Response},
};
use crate::{RustEmbed, resolve};
#[derive(Clone, Debug)]
pub struct RouterConfig {
pub never_decompress: bool,
}
impl Default for RouterConfig {
fn default() -> Self {
Self {
never_decompress: true,
}
}
}
pub fn router<A>() -> Router
where
A: RustEmbed + Send + Sync + 'static,
{
router_with::<A>(RouterConfig::default())
}
pub fn router_with<A>(config: RouterConfig) -> Router
where
A: RustEmbed + Send + Sync + 'static,
{
Router::new().fallback(handler::<A>).with_state(config)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Encoding {
RawAsIs,
GzippedAsIs,
Decompress,
}
fn pick_encoding(asset_gzipped: bool, accepts_gzip: bool, config: &RouterConfig) -> Encoding {
if !asset_gzipped {
Encoding::RawAsIs
} else if accepts_gzip || config.never_decompress {
Encoding::GzippedAsIs
} else {
Encoding::Decompress
}
}
async fn handler<A: RustEmbed>(
State(config): State<RouterConfig>,
uri: Uri,
headers: HeaderMap,
) -> Response {
let Some(asset) = resolve::<A>(uri.path()) else {
return (StatusCode::NOT_FOUND, "Not Found").into_response();
};
let mime = mime_guess::from_path(&asset.path).first_or_octet_stream();
let encoding = pick_encoding(asset.gzipped, accepts_gzip(&headers), &config);
match encoding {
Encoding::GzippedAsIs => Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, mime.as_ref())
.header(header::CONTENT_ENCODING, "gzip")
.header(header::VARY, "Accept-Encoding")
.body(Body::from(asset.data.into_owned()))
.unwrap(),
Encoding::Decompress => match asset.decoded() {
Ok(decoded) => Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, mime.as_ref())
.header(header::VARY, "Accept-Encoding")
.body(Body::from(decoded.into_owned()))
.unwrap(),
Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "decompression failed").into_response(),
},
Encoding::RawAsIs => Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, mime.as_ref())
.body(Body::from(asset.data.into_owned()))
.unwrap(),
}
}
fn accepts_gzip(headers: &HeaderMap) -> bool {
let Some(value) = headers.get(header::ACCEPT_ENCODING) else {
return false;
};
let Ok(s) = value.to_str() else {
return false;
};
s.split(',').any(|enc| {
let token = enc.split(';').next().unwrap_or("").trim();
token.eq_ignore_ascii_case("gzip")
})
}
#[cfg(test)]
mod tests {
use super::*;
fn h(value: &str) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert(header::ACCEPT_ENCODING, value.parse().unwrap());
headers
}
#[test]
fn missing_header_is_false() {
assert!(!accepts_gzip(&HeaderMap::new()));
}
#[test]
fn empty_header_is_false() {
assert!(!accepts_gzip(&h("")));
}
#[test]
fn plain_gzip_is_true() {
assert!(accepts_gzip(&h("gzip")));
}
#[test]
fn case_insensitive() {
assert!(accepts_gzip(&h("GZIP")));
assert!(accepts_gzip(&h("Gzip")));
}
#[test]
fn finds_gzip_anywhere_in_list() {
assert!(accepts_gzip(&h("gzip, deflate")));
assert!(accepts_gzip(&h("deflate, gzip")));
assert!(accepts_gzip(&h("br, gzip, deflate")));
}
#[test]
fn ignores_q_parameter() {
assert!(accepts_gzip(&h("gzip;q=0.5")));
assert!(accepts_gzip(&h("gzip; q=0.8")));
assert!(accepts_gzip(&h("deflate, gzip;q=0.9, br")));
}
#[test]
fn surrounding_whitespace_ok() {
assert!(accepts_gzip(&h(" gzip ")));
assert!(accepts_gzip(&h(" deflate , gzip ")));
}
#[test]
fn other_encodings_alone_are_false() {
assert!(!accepts_gzip(&h("deflate")));
assert!(!accepts_gzip(&h("br")));
assert!(!accepts_gzip(&h("identity")));
assert!(!accepts_gzip(&h("br, deflate, identity")));
}
#[test]
fn substring_match_is_not_enough() {
assert!(!accepts_gzip(&h("x-gzip")));
assert!(!accepts_gzip(&h("gzipped")));
}
fn default_cfg() -> RouterConfig {
RouterConfig::default()
}
fn allow_decompress_cfg() -> RouterConfig {
RouterConfig {
never_decompress: false,
}
}
#[test]
fn default_has_never_decompress_set() {
assert!(default_cfg().never_decompress);
}
#[test]
fn raw_assets_are_always_sent_as_is() {
assert_eq!(
pick_encoding(false, true, &default_cfg()),
Encoding::RawAsIs
);
assert_eq!(
pick_encoding(false, false, &default_cfg()),
Encoding::RawAsIs
);
assert_eq!(
pick_encoding(false, false, &allow_decompress_cfg()),
Encoding::RawAsIs
);
}
#[test]
fn gzipped_to_gzip_aware_client_is_sent_as_is() {
assert_eq!(
pick_encoding(true, true, &default_cfg()),
Encoding::GzippedAsIs
);
}
#[test]
fn default_config_sends_gzip_to_unaware_clients() {
assert_eq!(
pick_encoding(true, false, &default_cfg()),
Encoding::GzippedAsIs
);
}
#[test]
fn allow_decompress_falls_back_for_unaware_clients() {
assert_eq!(
pick_encoding(true, false, &allow_decompress_cfg()),
Encoding::Decompress
);
}
}