Skip to content

Commit 758c44c

Browse files
committed
fix: support range HEAD requests
1 parent 0a046bf commit 758c44c

1 file changed

Lines changed: 164 additions & 0 deletions

File tree

crates/core/src/proxy.rs

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,7 @@ where
477477
key,
478478
original_headers,
479479
&[
480+
"range",
480481
"if-match",
481482
"if-none-match",
482483
"if-modified-since",
@@ -742,3 +743,166 @@ fn build_object_path(config: &BucketConfig, key: &str) -> object_store::path::Pa
742743
None => object_store::path::Path::from(key),
743744
}
744745
}
746+
747+
#[cfg(test)]
748+
mod tests {
749+
use super::*;
750+
use crate::api::response::BucketEntry;
751+
use crate::backend::RawResponse;
752+
use crate::registry::{BucketRegistry, CredentialRegistry, ResolvedBucket};
753+
use crate::types::{ResolvedIdentity, RoleConfig, StoredCredential};
754+
use object_store::list::PaginatedListStore;
755+
use object_store::signer::Signer;
756+
use std::collections::HashMap;
757+
use std::sync::Arc;
758+
759+
// ── Mocks ───────────────────────────────────────────────────────
760+
761+
#[derive(Clone)]
762+
struct MockBackend;
763+
764+
impl ProxyBackend for MockBackend {
765+
fn create_paginated_store(
766+
&self,
767+
_config: &BucketConfig,
768+
) -> Result<Box<dyn PaginatedListStore>, ProxyError> {
769+
unimplemented!("not needed for forward tests")
770+
}
771+
772+
fn create_signer(&self, config: &BucketConfig) -> Result<Arc<dyn Signer>, ProxyError> {
773+
// Build a real S3 signer from the test config — produces a valid presigned URL.
774+
crate::backend::build_signer(config)
775+
}
776+
777+
async fn send_raw(
778+
&self,
779+
_method: http::Method,
780+
_url: String,
781+
_headers: HeaderMap,
782+
_body: Bytes,
783+
) -> Result<RawResponse, ProxyError> {
784+
unimplemented!("not needed for forward tests")
785+
}
786+
}
787+
788+
#[derive(Clone)]
789+
struct MockRegistry;
790+
791+
impl BucketRegistry for MockRegistry {
792+
async fn get_bucket(
793+
&self,
794+
name: &str,
795+
_identity: &ResolvedIdentity,
796+
_operation: &S3Operation,
797+
) -> Result<ResolvedBucket, ProxyError> {
798+
Ok(ResolvedBucket {
799+
config: test_bucket_config(name),
800+
list_rewrite: None,
801+
})
802+
}
803+
804+
async fn list_buckets(
805+
&self,
806+
_identity: &ResolvedIdentity,
807+
) -> Result<Vec<BucketEntry>, ProxyError> {
808+
Ok(vec![])
809+
}
810+
}
811+
812+
#[derive(Clone)]
813+
struct MockCreds;
814+
815+
impl CredentialRegistry for MockCreds {
816+
async fn get_credential(
817+
&self,
818+
_access_key_id: &str,
819+
) -> Result<Option<StoredCredential>, ProxyError> {
820+
Ok(None)
821+
}
822+
823+
async fn get_role(&self, _role_id: &str) -> Result<Option<RoleConfig>, ProxyError> {
824+
Ok(None)
825+
}
826+
}
827+
828+
fn test_bucket_config(name: &str) -> BucketConfig {
829+
let mut backend_options = HashMap::new();
830+
backend_options.insert(
831+
"endpoint".into(),
832+
"https://s3.us-east-1.amazonaws.com".into(),
833+
);
834+
backend_options.insert("bucket_name".into(), "backend-bucket".into());
835+
backend_options.insert("region".into(), "us-east-1".into());
836+
backend_options.insert("access_key_id".into(), "AKIAIOSFODNN7EXAMPLE".into());
837+
backend_options.insert(
838+
"secret_access_key".into(),
839+
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".into(),
840+
);
841+
BucketConfig {
842+
name: name.to_string(),
843+
backend_type: "s3".into(),
844+
backend_prefix: None,
845+
anonymous_access: true,
846+
allowed_roles: vec![],
847+
backend_options,
848+
}
849+
}
850+
851+
fn run<F: std::future::Future>(f: F) -> F::Output {
852+
futures::executor::block_on(f)
853+
}
854+
855+
fn gateway() -> ProxyGateway<MockBackend, MockRegistry, MockCreds> {
856+
ProxyGateway::new(MockBackend, MockRegistry, MockCreds, None)
857+
}
858+
859+
// ── Tests ───────────────────────────────────────────────────────
860+
861+
#[test]
862+
fn get_forward_preserves_range_header() {
863+
run(async {
864+
let gw = gateway();
865+
let mut headers = HeaderMap::new();
866+
headers.insert("range", "bytes=0-99".parse().unwrap());
867+
let action = gw
868+
.resolve_request(Method::GET, "/test-bucket/key.txt", None, &headers)
869+
.await;
870+
871+
match action {
872+
HandlerAction::Forward(fwd) => {
873+
assert_eq!(fwd.method, Method::GET);
874+
assert_eq!(
875+
fwd.headers.get("range").map(|v| v.to_str().unwrap()),
876+
Some("bytes=0-99"),
877+
"GET forward should pass through the Range header"
878+
);
879+
}
880+
other => panic!("expected Forward, got {:?}", std::mem::discriminant(&other)),
881+
}
882+
});
883+
}
884+
885+
#[test]
886+
fn head_forward_preserves_range_header() {
887+
run(async {
888+
let gw = gateway();
889+
let mut headers = HeaderMap::new();
890+
headers.insert("range", "bytes=0-1023".parse().unwrap());
891+
let action = gw
892+
.resolve_request(Method::HEAD, "/test-bucket/key.txt", None, &headers)
893+
.await;
894+
895+
match action {
896+
HandlerAction::Forward(fwd) => {
897+
assert_eq!(fwd.method, Method::HEAD);
898+
assert_eq!(
899+
fwd.headers.get("range").map(|v| v.to_str().unwrap()),
900+
Some("bytes=0-1023"),
901+
"HEAD forward should pass through the Range header"
902+
);
903+
}
904+
other => panic!("expected Forward, got {:?}", std::mem::discriminant(&other)),
905+
}
906+
});
907+
}
908+
}

0 commit comments

Comments
 (0)