or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

bucket-access-control.mdbucket-features.mdbucket-operations.mdclient-config.mdindex.mdlist-operations.mdmultipart-upload.mdobject-operations.mdpaginators-waiters.mdpresigned-urls.mdtypes.md
tile.json

presigned-urls.mddocs/

S3 Presigned URLs

Presigned URLs allow you to grant temporary access to S3 objects without requiring AWS credentials. The AWS SDK for Go v2 S3 package provides comprehensive support for generating presigned URLs for various operations.

Overview

Presigned URLs are signed URLs that provide temporary access to S3 resources. They include authentication information in the URL query string, allowing clients to perform S3 operations without AWS credentials.

Key concepts:

  • Presigned URLs contain AWS Signature Version 4 (SigV4) authentication in the URL
  • URLs have a configurable expiration time (default: 15 minutes, maximum: 7 days)
  • Both GET-style URLs and POST form uploads are supported
  • Ideal for browser uploads, temporary downloads, and sharing objects securely

PresignClient Type

Constructor { .api }

func NewPresignClient(c *Client, optFns ...func(*PresignOptions)) *PresignClient

Creates a new presign client for generating presigned URLs.

Parameters:

  • c - The S3 client to use for presigning operations
  • optFns - Optional configuration functions to customize presign behavior

Returns:

  • *PresignClient - A presign client configured with the provided options

Example:

import (
    "context"
    "time"

    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/s3"
)

// Load AWS configuration
cfg, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
    panic(err)
}

// Create S3 client
client := s3.NewFromConfig(cfg)

// Create presign client with default 1-hour expiration
presignClient := s3.NewPresignClient(client, func(po *s3.PresignOptions) {
    po.Expires = time.Hour
})

PresignOptions Type { .api }

type PresignOptions struct {
    // ClientOptions are list of functional options to mutate client options used by
    // the presign client.
    ClientOptions []func(*Options)

    // Presigner is the presigner used by the presign url client
    Presigner HTTPPresignerV4

    // Expires sets the expiration duration for the generated presign url. This should
    // be the duration in seconds the presigned URL should be considered valid for. If
    // not set or set to zero, presign url would default to expire after 900 seconds.
    Expires time.Duration

    // presignerV4a is the presigner used by the presign url client
    presignerV4a httpPresignerV4a
}

Configuration options for presigning operations.

Fields:

  • ClientOptions - []func(*Options) List of functional options to configure the underlying S3 client used for presigning. Applied when creating presigned URLs.

  • Presigner - HTTPPresignerV4 The SigV4 presigner implementation. If not provided, a default presigner is created automatically.

  • Expires - time.Duration Duration for which the presigned URL remains valid. Default is 15 minutes (900 seconds). Maximum allowed is 7 days for SigV4.

  • presignerV4a - httpPresignerV4a (internal) SigV4A presigner for multi-region access points. Created automatically if needed.

Configuration Functions

WithPresignClientFromClientOptions { .api }

func WithPresignClientFromClientOptions(optFns ...func(*Options)) func(*PresignOptions)

Helper function to apply S3 client options to presign operations.

Parameters:

  • optFns - S3 client option functions to apply

Returns:

  • Function that applies the options to PresignOptions

Example:

// Create presign client with custom endpoint
presignClient := s3.NewPresignClient(
    client,
    s3.WithPresignClientFromClientOptions(func(o *s3.Options) {
        o.UsePathStyle = true
        o.BaseEndpoint = aws.String("https://custom-s3-endpoint.com")
    }),
)

WithPresignExpires { .api }

func WithPresignExpires(dur time.Duration) func(*PresignOptions)

Helper function to set the expiration duration for presigned URLs.

Parameters:

  • dur - Duration for which the presigned URL should be valid

Returns:

  • Function that sets the expiration on PresignOptions

Example:

// Generate presigned URL valid for 2 hours
presignedReq, err := presignClient.PresignGetObject(
    ctx,
    &s3.GetObjectInput{
        Bucket: aws.String("my-bucket"),
        Key:    aws.String("my-key"),
    },
    s3.WithPresignExpires(2*time.Hour),
)

WithSigV4SigningName { .api }

func WithSigV4SigningName(name string) func(*Options)

Overrides the SigV4 signing service name. Rarely needed for standard S3 operations.

WithSigV4SigningRegion { .api }

func WithSigV4SigningRegion(region string) func(*Options)

Overrides the SigV4 signing region. Rarely needed for standard S3 operations.

WithSigV4ASigningRegions { .api }

func WithSigV4ASigningRegions(regions []string) func(*Options)

Sets regions for SigV4A multi-region signing, used with Multi-Region Access Points.

Presign Methods

PresignGetObject { .api }

func (c *PresignClient) PresignGetObject(
    ctx context.Context,
    params *GetObjectInput,
    optFns ...func(*PresignOptions),
) (*v4.PresignedHTTPRequest, error)

Generates a presigned URL for downloading an object from S3.

Parameters:

  • ctx - Context for the request
  • params - GetObject input parameters (bucket, key, version, etc.)
  • optFns - Optional presign configuration functions

Returns:

  • *v4.PresignedHTTPRequest - Presigned request containing URL and headers
  • error - Any error encountered during presigning

Example:

import (
    "context"
    "fmt"
    "time"

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/s3"
)

func generateDownloadURL(bucket, key string) (string, error) {
    cfg, err := config.LoadDefaultConfig(context.TODO())
    if err != nil {
        return "", err
    }

    client := s3.NewFromConfig(cfg)
    presignClient := s3.NewPresignClient(client)

    // Generate presigned GET URL valid for 15 minutes
    presignedReq, err := presignClient.PresignGetObject(
        context.TODO(),
        &s3.GetObjectInput{
            Bucket: aws.String(bucket),
            Key:    aws.String(key),
        },
        s3.WithPresignExpires(15*time.Minute),
    )
    if err != nil {
        return "", err
    }

    fmt.Println("Presigned URL:", presignedReq.URL)
    fmt.Println("HTTP Method:", presignedReq.Method)

    return presignedReq.URL, nil
}

Use cases:

  • Temporary download links for authenticated users
  • Direct browser downloads without proxying through application
  • Sharing private objects with expiration
  • Content delivery with access control

PresignPutObject { .api }

func (c *PresignClient) PresignPutObject(
    ctx context.Context,
    params *PutObjectInput,
    optFns ...func(*PresignOptions),
) (*v4.PresignedHTTPRequest, error)

Generates a presigned URL for uploading an object to S3.

Parameters:

  • ctx - Context for the request
  • params - PutObject input parameters (bucket, key, metadata, etc.)
  • optFns - Optional presign configuration functions

Returns:

  • *v4.PresignedHTTPRequest - Presigned request containing URL and headers
  • error - Any error encountered during presigning

Example:

func generateUploadURL(bucket, key string) (string, error) {
    cfg, err := config.LoadDefaultConfig(context.TODO())
    if err != nil {
        return "", err
    }

    client := s3.NewFromConfig(cfg)
    presignClient := s3.NewPresignClient(client)

    // Generate presigned PUT URL
    presignedReq, err := presignClient.PresignPutObject(
        context.TODO(),
        &s3.PutObjectInput{
            Bucket: aws.String(bucket),
            Key:    aws.String(key),
        },
        s3.WithPresignExpires(30*time.Minute),
    )
    if err != nil {
        return "", err
    }

    fmt.Println("Upload URL:", presignedReq.URL)
    fmt.Println("Use HTTP PUT to upload to this URL")

    // Client would use: PUT presignedReq.URL with file data

    return presignedReq.URL, nil
}

Use cases:

  • Browser-based file uploads
  • Mobile app direct uploads
  • Bypassing application server for large uploads
  • Client-side upload with metadata

PresignDeleteObject { .api }

func (c *PresignClient) PresignDeleteObject(
    ctx context.Context,
    params *DeleteObjectInput,
    optFns ...func(*PresignOptions),
) (*v4.PresignedHTTPRequest, error)

Generates a presigned URL for deleting an object from S3.

Parameters:

  • ctx - Context for the request
  • params - DeleteObject input parameters (bucket, key, version, etc.)
  • optFns - Optional presign configuration functions

Returns:

  • *v4.PresignedHTTPRequest - Presigned request containing URL and headers
  • error - Any error encountered during presigning

Example:

func generateDeleteURL(bucket, key string) (string, error) {
    cfg, err := config.LoadDefaultConfig(context.TODO())
    if err != nil {
        return "", err
    }

    client := s3.NewFromConfig(cfg)
    presignClient := s3.NewPresignClient(client)

    // Generate presigned DELETE URL
    presignedReq, err := presignClient.PresignDeleteObject(
        context.TODO(),
        &s3.DeleteObjectInput{
            Bucket: aws.String(bucket),
            Key:    aws.String(key),
        },
    )
    if err != nil {
        return "", err
    }

    fmt.Println("Delete URL:", presignedReq.URL)
    fmt.Println("Use HTTP DELETE to remove this object")

    return presignedReq.URL, nil
}

PresignHeadObject { .api }

func (c *PresignClient) PresignHeadObject(
    ctx context.Context,
    params *HeadObjectInput,
    optFns ...func(*PresignOptions),
) (*v4.PresignedHTTPRequest, error)

Generates a presigned URL for retrieving object metadata without downloading the object body.

Parameters:

  • ctx - Context for the request
  • params - HeadObject input parameters (bucket, key, version, etc.)
  • optFns - Optional presign configuration functions

Returns:

  • *v4.PresignedHTTPRequest - Presigned request containing URL and headers
  • error - Any error encountered during presigning

Example:

func generateMetadataURL(bucket, key string) (string, error) {
    cfg, err := config.LoadDefaultConfig(context.TODO())
    if err != nil {
        return "", err
    }

    client := s3.NewFromConfig(cfg)
    presignClient := s3.NewPresignClient(client)

    // Generate presigned HEAD URL to check if object exists
    presignedReq, err := presignClient.PresignHeadObject(
        context.TODO(),
        &s3.HeadObjectInput{
            Bucket: aws.String(bucket),
            Key:    aws.String(key),
        },
    )
    if err != nil {
        return "", err
    }

    fmt.Println("Metadata URL:", presignedReq.URL)
    fmt.Println("Use HTTP HEAD to get object metadata")

    return presignedReq.URL, nil
}

Use cases:

  • Check if object exists
  • Retrieve content-type, size, and other metadata
  • Verify object accessibility
  • Pre-flight checks before download

PresignDeleteBucket { .api }

func (c *PresignClient) PresignDeleteBucket(
    ctx context.Context,
    params *DeleteBucketInput,
    optFns ...func(*PresignOptions),
) (*v4.PresignedHTTPRequest, error)

Generates a presigned URL for deleting a bucket.

Parameters:

  • ctx - Context for the request
  • params - DeleteBucket input parameters (bucket name, expected owner, etc.)
  • optFns - Optional presign configuration functions

Returns:

  • *v4.PresignedHTTPRequest - Presigned request containing URL and headers
  • error - Any error encountered during presigning

Example:

import (
    "context"
    "fmt"
    "time"

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/s3"
)

func generateDeleteBucketURL(bucket string) (string, error) {
    cfg, err := config.LoadDefaultConfig(context.TODO())
    if err != nil {
        return "", err
    }

    client := s3.NewFromConfig(cfg)
    presignClient := s3.NewPresignClient(client)

    // Generate presigned DELETE URL for bucket
    presignedReq, err := presignClient.PresignDeleteBucket(
        context.TODO(),
        &s3.DeleteBucketInput{
            Bucket: aws.String(bucket),
        },
        s3.WithPresignExpires(15*time.Minute),
    )
    if err != nil {
        return "", err
    }

    fmt.Println("Delete Bucket URL:", presignedReq.URL)
    fmt.Println("Use HTTP DELETE to remove this bucket")

    return presignedReq.URL, nil
}

Use cases:

  • Temporary bucket deletion permissions for administrators
  • Delegating bucket cleanup to external systems
  • Time-limited bucket deletion capabilities

Important notes:

  • The bucket must be empty before it can be deleted
  • Consider using this with caution as bucket deletion is irreversible
  • Ensure the presigned URL has appropriate expiration time

PresignHeadBucket { .api }

func (c *PresignClient) PresignHeadBucket(
    ctx context.Context,
    params *HeadBucketInput,
    optFns ...func(*PresignOptions),
) (*v4.PresignedHTTPRequest, error)

Generates a presigned URL for checking if a bucket exists and if you have permission to access it.

Parameters:

  • ctx - Context for the request
  • params - HeadBucket input parameters (bucket name, expected owner)
  • optFns - Optional presign configuration functions

Returns:

  • *v4.PresignedHTTPRequest - Presigned request containing URL and headers
  • error - Any error encountered during presigning

Example:

import (
    "context"
    "fmt"
    "time"

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/s3"
)

func generateCheckBucketURL(bucket string) (string, error) {
    cfg, err := config.LoadDefaultConfig(context.TODO())
    if err != nil {
        return "", err
    }

    client := s3.NewFromConfig(cfg)
    presignClient := s3.NewPresignClient(client)

    // Generate presigned HEAD URL to check bucket existence
    presignedReq, err := presignClient.PresignHeadBucket(
        context.TODO(),
        &s3.HeadBucketInput{
            Bucket: aws.String(bucket),
        },
        s3.WithPresignExpires(10*time.Minute),
    )
    if err != nil {
        return "", err
    }

    fmt.Println("Check Bucket URL:", presignedReq.URL)
    fmt.Println("Use HTTP HEAD to verify bucket existence and access")

    // Client would use: HEAD presignedReq.URL
    // HTTP 200 = bucket exists and accessible
    // HTTP 404 = bucket doesn't exist or no access

    return presignedReq.URL, nil
}

Advanced example with HTTP client:

import (
    "context"
    "fmt"
    "net/http"
    "time"

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/s3"
)

func checkBucketWithPresignedURL(bucket string) (bool, error) {
    cfg, err := config.LoadDefaultConfig(context.TODO())
    if err != nil {
        return false, err
    }

    client := s3.NewFromConfig(cfg)
    presignClient := s3.NewPresignClient(client)

    // Generate presigned HEAD request
    presignedReq, err := presignClient.PresignHeadBucket(
        context.TODO(),
        &s3.HeadBucketInput{
            Bucket: aws.String(bucket),
        },
        s3.WithPresignExpires(5*time.Minute),
    )
    if err != nil {
        return false, err
    }

    // Execute the presigned request
    httpReq, err := http.NewRequest(presignedReq.Method, presignedReq.URL, nil)
    if err != nil {
        return false, err
    }

    // Add signed headers
    for key, values := range presignedReq.SignedHeader {
        for _, value := range values {
            httpReq.Header.Add(key, value)
        }
    }

    httpClient := &http.Client{}
    resp, err := httpClient.Do(httpReq)
    if err != nil {
        return false, err
    }
    defer resp.Body.Close()

    // Check response status
    if resp.StatusCode == http.StatusOK {
        fmt.Println("Bucket exists and is accessible")
        return true, nil
    } else if resp.StatusCode == http.StatusNotFound {
        fmt.Println("Bucket does not exist or no access")
        return false, nil
    }

    return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}

Use cases:

  • Pre-flight checks before attempting operations
  • Bucket existence verification without listing contents
  • Permission testing for buckets
  • Monitoring and health check systems

PresignUploadPart { .api }

func (c *PresignClient) PresignUploadPart(
    ctx context.Context,
    params *UploadPartInput,
    optFns ...func(*PresignOptions),
) (*v4.PresignedHTTPRequest, error)

Generates a presigned URL for uploading a part in a multipart upload.

Parameters:

  • ctx - Context for the request
  • params - UploadPart input parameters (bucket, key, upload ID, part number, etc.)
  • optFns - Optional presign configuration functions

Returns:

  • *v4.PresignedHTTPRequest - Presigned request containing URL and headers
  • error - Any error encountered during presigning

Example:

func generateMultipartUploadURLs(bucket, key, uploadID string, numParts int) ([]string, error) {
    cfg, err := config.LoadDefaultConfig(context.TODO())
    if err != nil {
        return nil, err
    }

    client := s3.NewFromConfig(cfg)
    presignClient := s3.NewPresignClient(client)

    urls := make([]string, numParts)

    for i := 0; i < numParts; i++ {
        partNumber := int32(i + 1)

        // Generate presigned URL for each part
        presignedReq, err := presignClient.PresignUploadPart(
            context.TODO(),
            &s3.UploadPartInput{
                Bucket:     aws.String(bucket),
                Key:        aws.String(key),
                UploadId:   aws.String(uploadID),
                PartNumber: aws.Int32(partNumber),
            },
            s3.WithPresignExpires(time.Hour),
        )
        if err != nil {
            return nil, err
        }

        urls[i] = presignedReq.URL
        fmt.Printf("Part %d URL: %s\n", partNumber, presignedReq.URL)
    }

    return urls, nil
}

Use cases:

  • Large file uploads split into parts
  • Parallel upload from browser or mobile app
  • Resume-able uploads
  • Upload optimization with concurrent parts

POST Upload Methods

PresignPostObject { .api }

func (c *PresignClient) PresignPostObject(
    ctx context.Context,
    params *PutObjectInput,
    optFns ...func(*PresignPostOptions),
) (*PresignedPostRequest, error)

Generates form fields for browser-based POST upload to S3. This is different from other presign methods as it returns form fields rather than a URL with query parameters.

Parameters:

  • ctx - Context for the request
  • params - PutObject input parameters (bucket, key, metadata, etc.)
  • optFns - Optional POST presign configuration functions

Returns:

  • *PresignedPostRequest - Contains URL and form fields for POST upload
  • error - Any error encountered during presigning

Example:

import (
    "context"
    "fmt"
    "time"

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/s3"
)

func generatePostUploadForm(bucket, key string) error {
    cfg, err := config.LoadDefaultConfig(context.TODO())
    if err != nil {
        return err
    }

    client := s3.NewFromConfig(cfg)
    presignClient := s3.NewPresignClient(client)

    // Generate POST upload with conditions
    postRequest, err := presignClient.PresignPostObject(
        context.TODO(),
        &s3.PutObjectInput{
            Bucket: aws.String(bucket),
            Key:    aws.String(key),
        },
        func(opts *s3.PresignPostOptions) {
            opts.Expires = time.Hour
            opts.Conditions = []interface{}{
                // Restrict content type to images
                []interface{}{"starts-with", "$Content-Type", "image/"},
                // Set maximum file size (10MB)
                []interface{}{"content-length-range", 1, 10485760},
                // Set ACL
                map[string]string{"acl": "public-read"},
            }
        },
    )
    if err != nil {
        return err
    }

    // Generate HTML form
    fmt.Println("<!-- HTML Form for Browser Upload -->")
    fmt.Printf("<form action=\"%s\" method=\"post\" enctype=\"multipart/form-data\">\n", postRequest.URL)

    // Add hidden form fields
    for key, value := range postRequest.Values {
        fmt.Printf("  <input type=\"hidden\" name=\"%s\" value=\"%s\" />\n", key, value)
    }

    // File input
    fmt.Println("  <input type=\"file\" name=\"file\" />")
    fmt.Println("  <input type=\"submit\" value=\"Upload\" />")
    fmt.Println("</form>")

    return nil
}

Advanced example with metadata and conditions:

func generateAdvancedPostUpload(bucket, keyPrefix string) (*s3.PresignedPostRequest, error) {
    cfg, err := config.LoadDefaultConfig(context.TODO())
    if err != nil {
        return nil, err
    }

    client := s3.NewFromConfig(cfg)
    presignClient := s3.NewPresignClient(client)

    postRequest, err := presignClient.PresignPostObject(
        context.TODO(),
        &s3.PutObjectInput{
            Bucket: aws.String(bucket),
            Key:    aws.String(keyPrefix + "/${filename}"),
        },
        func(opts *s3.PresignPostOptions) {
            opts.Expires = 2 * time.Hour
            opts.Conditions = []interface{}{
                // Key must start with specified prefix
                []interface{}{"starts-with", "$key", keyPrefix},
                // Content-Type restrictions
                []interface{}{"starts-with", "$Content-Type", "image/"},
                // File size limits (1KB to 5MB)
                []interface{}{"content-length-range", 1024, 5242880},
                // Cache control
                map[string]string{"Cache-Control": "max-age=3600"},
                // Custom metadata
                map[string]string{"x-amz-meta-uploader": "web-app"},
            }
        },
    )
    if err != nil {
        return nil, err
    }

    return postRequest, nil
}

Use cases:

  • HTML form-based file uploads
  • Browser direct uploads without JavaScript
  • Mobile web uploads
  • Restricted uploads with policy conditions

PresignPostOptions Type { .api }

type PresignPostOptions struct {
    // ClientOptions are list of functional options to mutate client options used by
    // the presign client.
    ClientOptions []func(*Options)

    // PostPresigner to use. One will be created if none is provided
    PostPresigner PresignPost

    // Expires sets the expiration duration for the generated presign url. This should
    // be the duration in seconds the presigned URL should be considered valid for. If
    // not set or set to zero, presign url would default to expire after 900 seconds.
    Expires time.Duration

    // Conditions a list of extra conditions to pass to the policy document
    // Available conditions can be found at:
    // https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html#sigv4-PolicyConditions
    Conditions []interface{}
}

Configuration options for POST presign operations.

Fields:

  • ClientOptions - []func(*Options) S3 client configuration options to apply.

  • PostPresigner - PresignPost POST presigner implementation. Automatically created if not provided.

  • Expires - time.Duration Duration for which the POST upload remains valid. Default is 15 minutes (900 seconds).

  • Conditions - []interface{} Policy conditions to restrict uploads. Each condition can be:

    • Exact match: map[string]string{"key": "value"}
    • Starts-with: []interface{}{"starts-with", "$field", "prefix"}
    • Content length range: []interface{}{"content-length-range", min, max}

Common policy conditions:

  • key - Restrict object key/name
  • acl - Set object ACL (e.g., "public-read", "private")
  • Content-Type - Restrict file MIME type
  • content-length-range - Restrict file size (min, max bytes)
  • Cache-Control - Set caching headers
  • x-amz-meta-* - Custom metadata fields
  • x-amz-server-side-encryption - Require encryption
  • success_action_redirect - Redirect URL after successful upload
  • success_action_status - HTTP status code to return after success

Example conditions:

conditions := []interface{}{
    // Exact match for ACL
    map[string]string{"acl": "public-read"},

    // Key must start with "uploads/"
    []interface{}{"starts-with", "$key", "uploads/"},

    // Content-Type must start with "image/"
    []interface{}{"starts-with", "$Content-Type", "image/"},

    // File size between 1KB and 10MB
    []interface{}{"content-length-range", 1024, 10485760},

    // Custom metadata
    map[string]string{"x-amz-meta-uuid": "user123"},

    // Redirect after success
    map[string]string{"success_action_redirect": "https://example.com/success"},
}

PresignedPostRequest Type { .api }

type PresignedPostRequest struct {
    // Represents the Base URL to make a request to
    URL string

    // Values is a key-value map of values to be sent as FormData
    // these values are not encoded
    Values map[string]string
}

Result of POST presign operation containing URL and form fields.

Fields:

  • URL - string The S3 endpoint URL where the form should be posted.

  • Values - map[string]string Form fields to include in the POST request. These must be sent as form data along with the file.

    Standard fields include:

    • key - Object key/name
    • policy - Base64-encoded policy document
    • X-Amz-Algorithm - Signature algorithm (AWS4-HMAC-SHA256)
    • X-Amz-Credential - AWS credentials
    • X-Amz-Date - Signing date
    • X-Amz-Signature - Request signature
    • X-Amz-Security-Token - Session token (if using temporary credentials)

Usage in HTML form:

<form action="${URL}" method="post" enctype="multipart/form-data">
  <!-- Hidden fields from Values map -->
  <input type="hidden" name="key" value="${Values['key']}" />
  <input type="hidden" name="X-Amz-Algorithm" value="${Values['X-Amz-Algorithm']}" />
  <input type="hidden" name="X-Amz-Credential" value="${Values['X-Amz-Credential']}" />
  <input type="hidden" name="X-Amz-Date" value="${Values['X-Amz-Date']}" />
  <input type="hidden" name="policy" value="${Values['policy']}" />
  <input type="hidden" name="X-Amz-Signature" value="${Values['X-Amz-Signature']}" />

  <!-- File input must be last -->
  <input type="file" name="file" />
  <input type="submit" value="Upload" />
</form>

Usage with JavaScript Fetch API:

const formData = new FormData();

// Add all form fields from Values
Object.entries(presignedPost.Values).forEach(([key, value]) => {
    formData.append(key, value);
});

// Add file (must be last)
formData.append('file', fileInput.files[0]);

// POST to S3
const response = await fetch(presignedPost.URL, {
    method: 'POST',
    body: formData
});

if (response.ok) {
    console.log('Upload successful');
} else {
    console.error('Upload failed:', response.statusText);
}

Related Types

HTTPPresignerV4 Interface { .api }

type HTTPPresignerV4 interface {
    PresignHTTP(
        ctx context.Context,
        credentials aws.Credentials,
        r *http.Request,
        payloadHash string,
        service string,
        region string,
        signingTime time.Time,
        optFns ...func(*v4.SignerOptions),
    ) (url string, signedHeader http.Header, err error)
}

Interface for SigV4 presigning HTTP requests. Implemented by v4.Signer.

Parameters:

  • ctx - Context for the operation
  • credentials - AWS credentials to sign with
  • r - HTTP request to presign
  • payloadHash - SHA256 hash of request payload
  • service - AWS service name (e.g., "s3")
  • region - AWS region
  • signingTime - Time to use for signing
  • optFns - Signer configuration options

Returns:

  • url - Presigned URL string
  • signedHeader - HTTP headers that are part of the signature
  • err - Any error during presigning

PresignedHTTPRequest Type { .api }

// From github.com/aws/aws-sdk-go-v2/aws/signer/v4
type PresignedHTTPRequest struct {
    URL          string
    Method       string
    SignedHeader http.Header
}

Result of GET/PUT/DELETE/HEAD presign operations.

Fields:

  • URL - string The complete presigned URL including all query parameters. This URL can be used directly to make the HTTP request.

  • Method - string HTTP method for the request (GET, PUT, DELETE, or HEAD).

  • SignedHeader - http.Header HTTP headers that are part of the signature and must be included in the request. Typically includes Host and possibly custom headers.

Example usage:

// Generate presigned GET request
presignedReq, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{
    Bucket: aws.String("my-bucket"),
    Key:    aws.String("my-object.txt"),
})
if err != nil {
    panic(err)
}

// Use with http.Client
httpReq, err := http.NewRequest(presignedReq.Method, presignedReq.URL, nil)
if err != nil {
    panic(err)
}

// Add signed headers
for key, values := range presignedReq.SignedHeader {
    for _, value := range values {
        httpReq.Header.Add(key, value)
    }
}

// Execute request
httpClient := &http.Client{}
resp, err := httpClient.Do(httpReq)
if err != nil {
    panic(err)
}
defer resp.Body.Close()

PresignPost Interface { .api }

type PresignPost interface {
    PresignPost(
        credentials aws.Credentials,
        bucket string,
        key string,
        region string,
        service string,
        signingTime time.Time,
        conditions []interface{},
        expirationTime time.Time,
        optFns ...func(*v4.SignerOptions),
    ) (fields map[string]string, err error)
}

Interface for POST upload presigning. Implemented internally by the SDK.

Parameters:

  • credentials - AWS credentials for signing
  • bucket - S3 bucket name
  • key - Object key
  • region - AWS region
  • service - Service name (typically "s3")
  • signingTime - Current time for signing
  • conditions - Policy conditions
  • expirationTime - When the presigned POST expires
  • optFns - Signer options

Returns:

  • fields - Form fields to include in POST request
  • err - Any error during presigning

Complete Examples

Example 1: Simple Download Link

package main

import (
    "context"
    "fmt"
    "time"

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/s3"
)

func main() {
    // Load AWS configuration
    cfg, err := config.LoadDefaultConfig(context.TODO())
    if err != nil {
        panic(err)
    }

    // Create S3 client
    client := s3.NewFromConfig(cfg)

    // Create presign client
    presignClient := s3.NewPresignClient(client)

    // Generate presigned GET URL
    presignedReq, err := presignClient.PresignGetObject(
        context.TODO(),
        &s3.GetObjectInput{
            Bucket: aws.String("my-bucket"),
            Key:    aws.String("documents/report.pdf"),
        },
        s3.WithPresignExpires(15*time.Minute),
    )
    if err != nil {
        panic(err)
    }

    fmt.Println("Download link (valid for 15 minutes):")
    fmt.Println(presignedReq.URL)
}

Example 2: Browser Direct Upload

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "time"

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/s3"
)

func main() {
    // Load AWS configuration
    cfg, err := config.LoadDefaultConfig(context.TODO())
    if err != nil {
        panic(err)
    }

    // Create S3 client and presign client
    client := s3.NewFromConfig(cfg)
    presignClient := s3.NewPresignClient(client)

    // HTTP handler for upload endpoint
    http.HandleFunc("/get-upload-url", func(w http.ResponseWriter, r *http.Request) {
        filename := r.URL.Query().Get("filename")
        if filename == "" {
            http.Error(w, "filename required", http.StatusBadRequest)
            return
        }

        // Generate presigned PUT URL
        presignedReq, err := presignClient.PresignPutObject(
            context.TODO(),
            &s3.PutObjectInput{
                Bucket: aws.String("my-uploads"),
                Key:    aws.String("uploads/" + filename),
            },
            s3.WithPresignExpires(time.Hour),
        )
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }

        // Return URL to client
        json.NewEncoder(w).Encode(map[string]string{
            "url":    presignedReq.URL,
            "method": presignedReq.Method,
        })
    })

    fmt.Println("Server starting on :8080")
    http.ListenAndServe(":8080", nil)
}

Client-side JavaScript:

async function uploadFile(file) {
    // Get presigned URL from server
    const response = await fetch(`/get-upload-url?filename=${file.name}`);
    const { url, method } = await response.json();

    // Upload directly to S3
    const uploadResponse = await fetch(url, {
        method: method,
        body: file,
        headers: {
            'Content-Type': file.type
        }
    });

    if (uploadResponse.ok) {
        console.log('Upload successful!');
    } else {
        console.error('Upload failed:', uploadResponse.statusText);
    }
}

Example 3: HTML Form POST Upload

package main

import (
    "context"
    "fmt"
    "html/template"
    "net/http"
    "time"

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/s3"
)

const formTemplate = `
<!DOCTYPE html>
<html>
<head>
    <title>Upload to S3</title>
</head>
<body>
    <h1>Upload Image to S3</h1>
    <form action="{{.URL}}" method="post" enctype="multipart/form-data">
        {{range $key, $value := .Values}}
        <input type="hidden" name="{{$key}}" value="{{$value}}" />
        {{end}}
        <input type="file" name="file" accept="image/*" required />
        <button type="submit">Upload</button>
    </form>
</body>
</html>
`

func main() {
    cfg, err := config.LoadDefaultConfig(context.TODO())
    if err != nil {
        panic(err)
    }

    client := s3.NewFromConfig(cfg)
    presignClient := s3.NewPresignClient(client)

    http.HandleFunc("/upload-form", func(w http.ResponseWriter, r *http.Request) {
        // Generate presigned POST request
        postRequest, err := presignClient.PresignPostObject(
            context.TODO(),
            &s3.PutObjectInput{
                Bucket: aws.String("my-uploads"),
                Key:    aws.String("images/${filename}"),
            },
            func(opts *s3.PresignPostOptions) {
                opts.Expires = time.Hour
                opts.Conditions = []interface{}{
                    // Only allow images
                    []interface{}{"starts-with", "$Content-Type", "image/"},
                    // Max 5MB file size
                    []interface{}{"content-length-range", 1, 5242880},
                    // Set public-read ACL
                    map[string]string{"acl": "public-read"},
                }
            },
        )
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }

        // Render HTML form with presigned fields
        tmpl := template.Must(template.New("form").Parse(formTemplate))
        tmpl.Execute(w, postRequest)
    })

    fmt.Println("Upload form server starting on :8080")
    fmt.Println("Visit http://localhost:8080/upload-form")
    http.ListenAndServe(":8080", nil)
}

Example 4: Multipart Upload with Presigned URLs

package main

import (
    "context"
    "fmt"
    "time"

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/s3"
)

type MultipartUploadSession struct {
    UploadID string
    PartURLs []string
}

func createMultipartUploadSession(bucket, key string, numParts int) (*MultipartUploadSession, error) {
    cfg, err := config.LoadDefaultConfig(context.TODO())
    if err != nil {
        return nil, err
    }

    client := s3.NewFromConfig(cfg)

    // Initiate multipart upload
    createResp, err := client.CreateMultipartUpload(
        context.TODO(),
        &s3.CreateMultipartUploadInput{
            Bucket: aws.String(bucket),
            Key:    aws.String(key),
        },
    )
    if err != nil {
        return nil, err
    }

    uploadID := *createResp.UploadId

    // Create presign client
    presignClient := s3.NewPresignClient(client)

    // Generate presigned URLs for each part
    partURLs := make([]string, numParts)
    for i := 0; i < numParts; i++ {
        partNumber := int32(i + 1)

        presignedReq, err := presignClient.PresignUploadPart(
            context.TODO(),
            &s3.UploadPartInput{
                Bucket:     aws.String(bucket),
                Key:        aws.String(key),
                UploadId:   aws.String(uploadID),
                PartNumber: aws.Int32(partNumber),
            },
            s3.WithPresignExpires(time.Hour),
        )
        if err != nil {
            return nil, err
        }

        partURLs[i] = presignedReq.URL
    }

    return &MultipartUploadSession{
        UploadID: uploadID,
        PartURLs: partURLs,
    }, nil
}

func main() {
    // Create session with 5 parts
    session, err := createMultipartUploadSession(
        "my-bucket",
        "large-file.zip",
        5,
    )
    if err != nil {
        panic(err)
    }

    fmt.Println("Upload ID:", session.UploadID)
    fmt.Println("\nUpload each part to:")
    for i, url := range session.PartURLs {
        fmt.Printf("Part %d: %s\n", i+1, url)
    }

    fmt.Println("\nAfter uploading all parts, complete the multipart upload with CompleteMultipartUpload")
}

Example 5: Presigned URL with Custom Headers

package main

import (
    "context"
    "fmt"
    "time"

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/s3"
    "github.com/aws/aws-sdk-go-v2/service/s3/types"
)

func generateDownloadWithMetadata(bucket, key string) (string, error) {
    cfg, err := config.LoadDefaultConfig(context.TODO())
    if err != nil {
        return "", err
    }

    client := s3.NewFromConfig(cfg)
    presignClient := s3.NewPresignClient(client)

    // Generate presigned GET URL with custom response headers
    presignedReq, err := presignClient.PresignGetObject(
        context.TODO(),
        &s3.GetObjectInput{
            Bucket: aws.String(bucket),
            Key:    aws.String(key),
            // Override response headers
            ResponseContentType:        aws.String("application/pdf"),
            ResponseContentDisposition: aws.String("attachment; filename=\"report.pdf\""),
            ResponseCacheControl:       aws.String("no-cache"),
        },
        s3.WithPresignExpires(30*time.Minute),
    )
    if err != nil {
        return "", err
    }

    return presignedReq.URL, nil
}

func generateUploadWithEncryption(bucket, key string) (string, error) {
    cfg, err := config.LoadDefaultConfig(context.TODO())
    if err != nil {
        return "", err
    }

    client := s3.NewFromConfig(cfg)
    presignClient := s3.NewPresignClient(client)

    // Generate presigned PUT URL with server-side encryption
    presignedReq, err := presignClient.PresignPutObject(
        context.TODO(),
        &s3.PutObjectInput{
            Bucket:               aws.String(bucket),
            Key:                  aws.String(key),
            ServerSideEncryption: types.ServerSideEncryptionAes256,
            Metadata: map[string]string{
                "uploader": "admin",
                "purpose":  "backup",
            },
        },
        s3.WithPresignExpires(time.Hour),
    )
    if err != nil {
        return "", err
    }

    return presignedReq.URL, nil
}

func main() {
    // Download with custom headers
    downloadURL, err := generateDownloadWithMetadata("my-bucket", "data.csv")
    if err != nil {
        panic(err)
    }
    fmt.Println("Download URL:", downloadURL)

    // Upload with encryption and metadata
    uploadURL, err := generateUploadWithEncryption("my-bucket", "backup.tar.gz")
    if err != nil {
        panic(err)
    }
    fmt.Println("Upload URL:", uploadURL)
}

Best Practices

Security

  1. Use appropriate expiration times:

    • Short-lived URLs (5-15 minutes) for immediate use
    • Longer expiration (1-24 hours) for workflows
    • Never exceed 7 days
  2. Apply least privilege:

    • Only presign operations the user needs
    • Use policy conditions to restrict POST uploads
    • Verify user permissions before generating presigned URLs
  3. Use HTTPS:

    • Presigned URLs work over HTTP but should only be distributed over HTTPS
    • Credentials in query strings can be logged
  4. Consider additional validation:

    • Implement server-side callbacks for POST uploads
    • Use success_action_redirect to handle upload results
    • Verify uploaded objects after completion

Performance

  1. Cache presigned URLs:

    • Generate once and reuse until near expiration
    • Store in session or temporary cache
    • Regenerate when close to expiration
  2. Use appropriate method:

    • GET presign for downloads
    • PUT presign for single-part uploads
    • POST presign for browser form uploads
    • Multipart with PresignUploadPart for large files
  3. Parallel multipart uploads:

    • Split large files into 5MB-100MB parts
    • Upload parts concurrently
    • Use PresignUploadPart for each part

Error Handling

func safePresignGetObject(ctx context.Context, client *s3.PresignClient, bucket, key string) (string, error) {
    if bucket == "" || key == "" {
        return "", fmt.Errorf("bucket and key are required")
    }

    presignedReq, err := client.PresignGetObject(
        ctx,
        &s3.GetObjectInput{
            Bucket: aws.String(bucket),
            Key:    aws.String(key),
        },
        s3.WithPresignExpires(15*time.Minute),
    )
    if err != nil {
        // Log error with context
        return "", fmt.Errorf("failed to presign GET for %s/%s: %w", bucket, key, err)
    }

    if presignedReq.URL == "" {
        return "", fmt.Errorf("presigned URL is empty")
    }

    return presignedReq.URL, nil
}

Common Pitfalls

  1. Forgetting signed headers:

    • Some presigned URLs include signed headers
    • These headers must be included in the actual request
    • Check PresignedHTTPRequest.SignedHeader
  2. URL encoding issues:

    • Don't double-encode presigned URLs
    • Use the URL exactly as returned
    • Query parameters are already encoded
  3. Clock skew:

    • Ensure system clock is synchronized
    • AWS allows 15-minute clock skew
    • Use NTP to keep time accurate
  4. Expired credentials:

    • Presigned URLs use credentials at generation time
    • If using temporary credentials, ensure they're valid for presign duration
    • Regenerate URLs before credentials expire
  5. Wrong HTTP method:

    • Use the method specified in PresignedHTTPRequest.Method
    • GET presign requires HTTP GET
    • PUT presign requires HTTP PUT

Related Documentation

  • S3 Client Documentation
  • Multipart Upload Guide
  • AWS SigV4 Signing
  • S3 POST Policy