This design introduces S3 object downloads using pre-signed URLs in AWS SDK Java v2, providing feature parity with v1. Some customers have described a need for downloading S3 objects through pre-signed URLs while maintaining Client side SDK benefits like automatic retries, metrics collection, and typed response objects.
This document proposes how this functionality should be implemented in the Java SDK v2, addressing customer-requested features (GitHub Issue #2731, GitHub Issue #181) by reducing complexity and improving usability for temporary access scenarios.
Look at decision log here: Decision Log Section
The Java SDK team has decided to implement a separate AsyncPresignedUrlExtension. The team chose the helper API pattern over direct S3AsyncClient integration to maintain clean separation of concerns while preserving SDK functionality.
The design introduces a new helper API AsyncPresignedUrlExtension which can be instantiated via the existing S3AsyncClient. This extension provides a clean abstraction layer that preserves SDK benefits while handling the unique requirements of pre-signed URL requests.
This design implements GET/download functionality for presigned URLs for the S3AsyncClient, including multipart download support for large objects. The synchronous S3Client implementation is deferred to future work.
The v2 SDK will support a presigned URL extension for the async client that can leverage pre-signed URL downloads.
Instantiating from existing client:
// Async Presigned URL Extension
S3AsyncClient s3Client = S3AsyncClient.create();
AsyncPresignedUrlExtension presignExtension = s3Client.presignedUrlExtension();// Create presigned URL request
PresignedUrlDownloadRequest request = PresignedUrlDownloadRequest.builder()
.presignedUrl(presignedUrl)
.range("range=0-1024")
.build();
// Async usage
S3AsyncClient s3Client = S3AsyncClient.create();
AsyncPresignedUrlExtension presignedUrlExtension = s3Client.presignedUrlExtension();
CompletableFuture<GetObjectResponse> response = presignedUrlExtension.getObject(request, AsyncResponseTransformer.toBytes());For objects with size greater than minimumPartSizeInBytes, the SDK automatically uses multipart downloads with HTTP Range headers when multipart is enabled:
// Enable multipart downloads
S3AsyncClient s3Client = S3AsyncClient.builder()
.multipartEnabled(true)
.build();
// Or with custom configuration
MultipartConfiguration config = MultipartConfiguration.builder()
.minimumPartSizeInBytes(8 * 1024 * 1024) // 8MB parts
.build();
S3AsyncClient multipartClient = S3AsyncClient.builder()
.multipartConfiguration(config)
.build();
// Download automatically uses multipart for objects larger than the configured part size
CompletableFuture<ResponseBytes<GetObjectResponse>> response =
multipartClient.presignedUrlExtension().getObject(request, AsyncResponseTransformer.toBytes());The multipart implementation uses Range headers (e.g., bytes=0-8388607) instead of partNumber parameters to preserve presigned URL signatures. The first request downloads the initial part while discovering total object size from the Content-Range header.
/**
* Interface for presigned URL operations used by Async clients.
*/
@SdkPublicApi
public interface AsyncPresignedUrlExtension {
/**
* Downloads S3 objects using pre-signed URLs with custom response transformation.
*
* @param request the presigned URL request.
* @param responseTransformer custom transformer for processing the response.
* @return a CompletableFuture of the transformed response.
*/
<T> CompletableFuture<T> getObject(PresignedUrlDownloadRequest request,
AsyncResponseTransformer<GetObjectResponse, T> responseTransformer);
// Additional getObject() overloads for file downloads, byte arrays, etc.
// Standard Builder interface with client() and overrideConfiguration() methods
}/**
* Request object for presigned URL GET operations.
*/
@SdkPublicApi
@ThreadSafe
public final class PresignedUrlDownloadRequest
implements ToCopyableBuilder<PresignedUrlDownloadRequest.Builder, PresignedUrlDownloadRequest> {
private final URL presignedUrl;
private final String range;
// Standard getters: presignedUrl(), range()
// Standard builder methods: builder(), toBuilder()
// Standard Builder class with presignedUrl(), range() setter methods
}Three approaches were considered:
-
Dedicated AsyncPresignedUrlExtension (CHOSEN): Separate extension accessed via
s3AsyncClient.presignedUrlExtension()- Pros: Clean separation, preserves SDK features, follows v2 patterns
- Cons: New API surface for users to learn
-
Direct S3Client Integration: Add presigned URL methods directly to S3Client
- Pros: Familiar interface, direct migration path from v1
- Cons: Requires core interceptor changes, complex integration, could confuse users by mixing presigned URL APIs with standard service-generated APIs
-
S3Presigner Extension: Extend existing S3Presigner to execute URLs
- Pros: Logical extension of presigner concept
- Cons: Breaks current stateless presigner patterns
Decision: Option 1 provides clean separation while preserving SDK benefits and following established v2 utility patterns.
The synchronous S3Client implementation has been deferred to future work due to complexities around multipart download requirements. Support for S3CrtAsyncClient will also be added in future work once the AWS CRT team addresses current limitations with pre-signed URL handling.
While extending S3Request would provide access to RequestOverrideConfiguration, many of these configurations (like credentials provider, signers) are not supported with presigned URL execution. Instead, we use a standalone request with only essential parameters (presignedUrl, range). Internally, this gets wrapped in an encapsulated class that extends S3Request for use with ClientHandler.
GitHub feature requests:
AWS Documentation:
SDK Documentation: