Skip to content

Commit e1dd6c7

Browse files
Bypass Fastly IO for SVG asset routes
1 parent 82290c1 commit e1dd6c7

1 file changed

Lines changed: 170 additions & 7 deletions

File tree

crates/trusted-server-core/src/proxy.rs

Lines changed: 170 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,11 @@ fn build_asset_proxy_target_url(
673673
Ok(target_url)
674674
}
675675

676+
fn asset_path_skips_image_optimizer(target_url: &url::Url) -> bool {
677+
let lower_path = target_url.path().to_ascii_lowercase();
678+
lower_path.ends_with(".svg") || lower_path.ends_with(".svgz")
679+
}
680+
676681
fn asset_origin_host_header(
677682
target_url: &url::Url,
678683
) -> Result<HeaderValue, Report<TrustedServerError>> {
@@ -949,8 +954,16 @@ pub async fn handle_asset_proxy_request(
949954
) -> Result<AssetProxyResponse, Report<TrustedServerError>> {
950955
let incoming_query = req.get_query_str().unwrap_or("");
951956
let mut target_url = build_asset_proxy_target_url(route, req.get_path(), incoming_query)?;
952-
let image_optimizer =
953-
crate::asset_image_optimizer::options_for_asset_request(settings, route, incoming_query)?;
957+
let skip_image_optimizer = asset_path_skips_image_optimizer(&target_url);
958+
let image_optimizer = if skip_image_optimizer {
959+
log::debug!(
960+
"Skipping Image Optimizer for unsupported SVG asset path: {}",
961+
target_url.path()
962+
);
963+
None
964+
} else {
965+
crate::asset_image_optimizer::options_for_asset_request(settings, route, incoming_query)?
966+
};
954967

955968
if route.origin_query_policy() == OriginQueryPolicy::Strip {
956969
target_url.set_query(None);
@@ -1791,7 +1804,7 @@ mod tests {
17911804
use std::sync::{Arc, Mutex};
17921805

17931806
use super::{
1794-
asset_origin_host_header, build_asset_proxy_target_url,
1807+
asset_origin_host_header, asset_path_skips_image_optimizer, build_asset_proxy_target_url,
17951808
clear_s3_credentials_cache_for_tests, handle_asset_proxy_request, handle_first_party_click,
17961809
handle_first_party_proxy, handle_first_party_proxy_rebuild, handle_first_party_proxy_sign,
17971810
is_host_allowed, proxy_request, rebuild_response_with_body,
@@ -1810,7 +1823,7 @@ mod tests {
18101823
use crate::settings::{
18111824
AssetImageOptimizerConfig, AssetOriginAuth, ImageOptimizerAspectRatioConfig,
18121825
ImageOptimizerCropOffsetsConfig, ImageOptimizerProfileSet, ImageOptimizerSettings,
1813-
OriginQueryPolicy, ProxyAssetRoute, S3SigV4AuthConfig,
1826+
OriginQueryPolicy, ProxyAssetRoute, S3SigV4AuthConfig, UnknownProfilePolicy,
18141827
};
18151828
use crate::test_support::tests::create_test_settings;
18161829
use crate::{
@@ -2872,6 +2885,36 @@ mod tests {
28722885
);
28732886
}
28742887

2888+
#[test]
2889+
fn asset_path_skips_image_optimizer_for_svg_extensions() {
2890+
for url in [
2891+
"https://assets.example.com/.images/logo.svg",
2892+
"https://assets.example.com/.images/LOGO.SVG",
2893+
"https://assets.example.com/.images/icon.svgz",
2894+
] {
2895+
let target_url = url::Url::parse(url).expect("should parse target URL");
2896+
assert!(
2897+
asset_path_skips_image_optimizer(&target_url),
2898+
"should skip Image Optimizer for {url}"
2899+
);
2900+
}
2901+
}
2902+
2903+
#[test]
2904+
fn asset_path_uses_image_optimizer_for_raster_extensions() {
2905+
for url in [
2906+
"https://assets.example.com/.images/photo.jpg",
2907+
"https://assets.example.com/.images/photo.png",
2908+
"https://assets.example.com/.images/photo.webp",
2909+
] {
2910+
let target_url = url::Url::parse(url).expect("should parse target URL");
2911+
assert!(
2912+
!asset_path_skips_image_optimizer(&target_url),
2913+
"should allow Image Optimizer for {url}"
2914+
);
2915+
}
2916+
}
2917+
28752918
#[test]
28762919
fn asset_origin_host_header_omits_standard_port() {
28772920
let target_url = url::Url::parse("https://assets.example.com/.images/foo.jpg")
@@ -3299,6 +3342,126 @@ mod tests {
32993342
assert_eq!((crop.offset_x, crop.offset_y), (Some(70), Some(50)));
33003343
}
33013344

3345+
#[tokio::test]
3346+
async fn handle_asset_proxy_request_skips_image_optimizer_for_svg_assets() {
3347+
let stub = Arc::new(StubHttpClient::new());
3348+
stub.push_response(200, b"ok".to_vec());
3349+
let services = build_services_with_http_client(
3350+
Arc::clone(&stub) as Arc<dyn crate::platform::PlatformHttpClient>
3351+
);
3352+
let mut settings = create_test_settings();
3353+
let mut profile_set = test_profile_set();
3354+
profile_set.unknown_profile = UnknownProfilePolicy::Reject;
3355+
settings.image_optimizer = ImageOptimizerSettings {
3356+
profile_sets: HashMap::from([("default_images".to_string(), profile_set)]),
3357+
};
3358+
let req = Request::new(
3359+
Method::GET,
3360+
"https://www.example.com/.images/logo.SVG?profile=unknown&ar=1-1",
3361+
);
3362+
let mut route = ProxyAssetRoute::new("/.images/", "https://assets.example.com");
3363+
route.image_optimizer = Some(AssetImageOptimizerConfig {
3364+
enabled: true,
3365+
region: "us_east".to_string(),
3366+
profile_set: "default_images".to_string(),
3367+
origin_query: None,
3368+
});
3369+
3370+
handle_asset_proxy_request(&settings, &services, req, &route)
3371+
.await
3372+
.expect("should proxy SVG asset without Image Optimizer profile parsing");
3373+
3374+
assert_eq!(
3375+
stub.recorded_request_uris(),
3376+
vec!["https://assets.example.com/.images/logo.SVG"],
3377+
"should still strip profile-table query from SVG origin requests"
3378+
);
3379+
assert_eq!(
3380+
stub.recorded_image_optimizer_options(),
3381+
vec![None],
3382+
"SVG assets should bypass Image Optimizer metadata"
3383+
);
3384+
}
3385+
3386+
#[tokio::test]
3387+
async fn handle_asset_proxy_request_skips_image_optimizer_for_rewritten_svg_assets() {
3388+
let stub = Arc::new(StubHttpClient::new());
3389+
stub.push_response(200, b"ok".to_vec());
3390+
let services = build_services_with_http_client(
3391+
Arc::clone(&stub) as Arc<dyn crate::platform::PlatformHttpClient>
3392+
);
3393+
let mut settings = create_test_settings();
3394+
settings.image_optimizer = ImageOptimizerSettings {
3395+
profile_sets: HashMap::from([("default_images".to_string(), test_profile_set())]),
3396+
};
3397+
let req = Request::new(
3398+
Method::GET,
3399+
"https://www.example.com/.image/options/id/logo.svg?profile=medium",
3400+
);
3401+
let mut route = ProxyAssetRoute::new("/.image/", "https://assets.example.com");
3402+
route.path_pattern = Some(r"^/\.image/(.*)/[^/]+\.([^/.]+)$".to_string());
3403+
route.target_path = Some("/image/upload/$1.$2".to_string());
3404+
route.image_optimizer = Some(AssetImageOptimizerConfig {
3405+
enabled: true,
3406+
region: "us_east".to_string(),
3407+
profile_set: "default_images".to_string(),
3408+
origin_query: None,
3409+
});
3410+
3411+
handle_asset_proxy_request(&settings, &services, req, &route)
3412+
.await
3413+
.expect("should proxy rewritten SVG asset without Image Optimizer metadata");
3414+
3415+
assert_eq!(
3416+
stub.recorded_request_uris(),
3417+
vec!["https://assets.example.com/image/upload/options/id.svg"],
3418+
"should detect SVG after route path rewriting"
3419+
);
3420+
assert_eq!(
3421+
stub.recorded_image_optimizer_options(),
3422+
vec![None],
3423+
"rewritten SVG assets should bypass Image Optimizer metadata"
3424+
);
3425+
}
3426+
3427+
#[tokio::test]
3428+
async fn handle_asset_proxy_request_skips_s3_preflight_for_svg_image_optimizer_routes() {
3429+
let stub = Arc::new(StubHttpClient::new());
3430+
stub.push_response(200, b"raw-svg".to_vec());
3431+
let services = build_services_with_secret_and_http_client(
3432+
HashMapSecretStore::new(test_s3_secrets()),
3433+
Arc::clone(&stub) as Arc<dyn crate::platform::PlatformHttpClient>,
3434+
);
3435+
let settings = create_test_settings();
3436+
let req = Request::new(
3437+
Method::GET,
3438+
"https://www.example.com/.images/logo.svg?profile=medium",
3439+
);
3440+
let route = test_s3_image_optimizer_route();
3441+
3442+
let mut response = handle_asset_proxy_request(&settings, &services, req, &route)
3443+
.await
3444+
.expect("should proxy raw SVG asset through S3 route")
3445+
.into_response();
3446+
3447+
assert_eq!(response.take_body_str(), "raw-svg");
3448+
assert_eq!(
3449+
stub.recorded_request_methods(),
3450+
vec!["GET"],
3451+
"SVG IO bypass should not add an S3 preflight"
3452+
);
3453+
assert_eq!(
3454+
stub.recorded_request_uris(),
3455+
vec!["https://examplebucket.s3.us-east-1.amazonaws.com/.images/logo.svg"],
3456+
"SVG IO bypass should still strip the transform query"
3457+
);
3458+
assert_eq!(
3459+
stub.recorded_image_optimizer_options(),
3460+
vec![None],
3461+
"SVG IO bypass should not attach Image Optimizer metadata"
3462+
);
3463+
}
3464+
33023465
#[tokio::test]
33033466
async fn handle_asset_proxy_request_preflights_s3_before_image_optimizer() {
33043467
let stub = Arc::new(StubHttpClient::new());
@@ -3361,7 +3524,7 @@ mod tests {
33613524
stub.push_response(404, Vec::new());
33623525
stub.push_response_with_headers(
33633526
404,
3364-
br#"<Error><Code>NoSuchKey</Code><Key>image/upload/missing.svg</Key></Error>"#.to_vec(),
3527+
br#"<Error><Code>NoSuchKey</Code><Key>image/upload/missing.jpg</Key></Error>"#.to_vec(),
33653528
vec![
33663529
(header::CONTENT_TYPE.as_str(), "application/xml"),
33673530
(header::CACHE_CONTROL.as_str(), "public, max-age=3600"),
@@ -3378,7 +3541,7 @@ mod tests {
33783541
};
33793542
let req = Request::new(
33803543
Method::GET,
3381-
"https://www.example.com/.images/missing.svg?profile=medium",
3544+
"https://www.example.com/.images/missing.jpg?profile=medium",
33823545
);
33833546
let route = test_s3_image_optimizer_route();
33843547

@@ -3405,7 +3568,7 @@ mod tests {
34053568
let body = response.take_body_str();
34063569
assert!(body.contains("NoSuchKey"), "should return S3 error body");
34073570
assert!(
3408-
body.contains("image/upload/missing.svg"),
3571+
body.contains("image/upload/missing.jpg"),
34093572
"should expose the missing S3 key"
34103573
);
34113574
assert_eq!(

0 commit comments

Comments
 (0)