fix(io): respect custom S3 endpoints from AWS profiles - #8390
fix(io): respect custom S3 endpoints from AWS profiles#8390lance-gatefixer[bot] wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
❌ Gate recommendation: request changes.
Profile configuration needs to be treated as an endpoint fallback at the same boundary as existing S3 resolution. The effective endpoint and region currently escape that boundary, so established AWS routing and endpoint-derived store behavior are not preserved.
A viable revision would resolve the effective endpoint before discovery and capability calculation, preserve explicit storage-option precedence, retain bucket-region discovery when no custom endpoint exists, and use the profile region only when the selected custom endpoint needs it.
| .entry(AmazonS3ConfigKey::Endpoint) | ||
| .or_insert_with(|| endpoint.to_string()); | ||
| } | ||
| if let Some(region) = profile_config.region() { |
There was a problem hiding this comment.
This makes the profile default region authoritative even when the profile has no custom endpoint. Before this change, a region-only profile reaches resolve_bucket_region, so a bucket outside that default uses its actual x-amz-bucket-region. Now Lance builds and signs the client for the profile region, and object_store treats the resulting bare cross-region redirect as an error.
Only import the profile region when an effective custom endpoint is selected. With no custom endpoint, retain bucket-region discovery.
Reproducer
I added this regression and ran cargo test -p lance-io review_region_only_profile_preserves_bucket_discovery --lib -- --test-threads=1:
#[tokio::test]
async fn review_region_only_profile_preserves_bucket_discovery() {
let profile_files = ProfileFiles::builder()
.with_contents(
ProfileFileKind::Config,
"[profile selected]\nregion = us-east-1",
)
.build();
let profile_config = aws_config::defaults(BehaviorVersion::latest())
.profile_name("selected")
.profile_files(profile_files)
.load()
.await;
let url = Url::parse("s3://please-dont-exist/path").unwrap();
let result = resolve_s3_region(&url, &mut HashMap::new(), Some(&profile_config)).await;
assert!(result.is_err(), "a region-only profile must retain discovery");
}Observed: the assertion failed because the call returned the profile region instead of attempting discovery.
There was a problem hiding this comment.
Addressed in 4f7b4db. Region-only profiles now leave the effective endpoint unset, so bucket-region discovery remains authoritative; the profile region is used only when a custom endpoint is selected. Added a deterministic regression for this routing path.
| let mut s3_storage_options = storage_options.as_s3_options(); | ||
| let region = resolve_s3_region(base_path, &s3_storage_options).await?; | ||
| let profile_config = if std::env::var_os("AWS_PROFILE").is_some() { | ||
| Some(aws_config::load_defaults(BehaviorVersion::latest()).await) |
There was a problem hiding this comment.
The profile endpoint is resolved only inside build_amazon_s3_store, after new_store has computed use_constant_size_upload_parts from the original options. For an R2 profile endpoint, requests therefore reach R2 while the flag remains false. ObjectWriter changes part size after part 100, but the repository contract says R2 requires constant-size parts, so sufficiently large uploads fail.
Resolve the effective endpoint before any endpoint-derived behavior, or return it to new_store, and derive the R2 capability from that effective endpoint.
Reproducer
I added an end-to-end profile regression and ran cargo test -p lance-io review_profile_r2_endpoint_enables_constant_parts --lib -- --test-threads=1:
#[tokio::test]
async fn review_profile_r2_endpoint_enables_constant_parts() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config");
std::fs::write(
&config_path,
"[profile gate-review-r2]\nregion = auto\nendpoint_url = https://account.r2.cloudflarestorage.com",
)
.unwrap();
let keys = [
"AWS_PROFILE", "AWS_CONFIG_FILE", "AWS_REGION",
"AWS_DEFAULT_REGION", "AWS_ENDPOINT_URL",
"AWS_IGNORE_CONFIGURED_ENDPOINT_URLS",
];
let saved = keys.map(|key| (key, std::env::var_os(key)));
unsafe {
std::env::set_var("AWS_PROFILE", "gate-review-r2");
std::env::set_var("AWS_CONFIG_FILE", &config_path);
for key in &keys[2..] { std::env::remove_var(key); }
}
let result = AwsStoreProvider
.new_store(Url::parse("s3://test-bucket/path").unwrap(), &ObjectStoreParams::default())
.await;
for (key, value) in saved {
unsafe {
match value {
Some(value) => std::env::set_var(key, value),
None => std::env::remove_var(key),
}
}
}
assert!(result.unwrap().use_constant_size_upload_parts);
}Observed: the final assertion failed; the profile endpoint was used but the R2 capability stayed disabled.
There was a problem hiding this comment.
Addressed in 4f7b4db. The effective profile endpoint is now resolved before capability calculation, and R2 detection uses that same resolved endpoint to enable constant-size multipart uploads. Added R2 profile regression coverage.
There was a problem hiding this comment.
✅ Gate recommendation: approve.
The revised endpoint-resolution boundary preserves explicit storage options, keeps bucket-region discovery authoritative for region-only profiles, and derives endpoint-specific behavior from the same effective endpoint. This resolves the prior routing failures without changing credential precedence or storage compatibility.
Summary
Root cause
S3 initialization only checked explicit storage options before querying the bucket at the default AWS endpoint. The AWS SDK profile chain was consulted later for credentials and a fallback signing region, after the incorrect bucket lookup and capability calculation had already occurred.
Validation
cargo test -p lance-io object_store::providers::aws::tests --lib(23 passed)cargo fmt --all -- --checkcargo clippy --all --tests --benches -- -D warningsFixes #3512