benbjohnson/litestream · error

s3: put object %s: %w

Error message

s3: put object %s: %w

What it means

The single-put path (files smaller than partSize) calls the S3 PutObject API; any API-level failure is wrapped in this error. Causes include network failures, expired/invalid credentials, missing bucket, permissions, or S3 5xx errors. The returned error preserves the AWS SDK error for inspection via errors.As/unwrap.

Source

Thrown at s3/replica_client.go:804

	// Extract timestamp from LTX header, then rewind so the upload sees the
	// full file.
	hdr, _, err := ltx.PeekHeader(rs)
	if err != nil {
		return 0, time.Time{}, nil, fmt.Errorf("extract timestamp from LTX header: %w", err)
	}
	timestamp := time.UnixMilli(hdr.Timestamp).UTC()
	if _, err := rs.Seek(start, io.SeekStart); err != nil {
		return 0, time.Time{}, nil, fmt.Errorf("s3: rewind ltx file %s: %w", key, err)
	}

	input := c.putObjectInput(key, timestamp)
	input.Body = rs

	if size < partSize {
		input.ContentLength = aws.Int64(size)
		out, err := c.s3.PutObject(ctx, input)
		if err != nil {
			return 0, time.Time{}, nil, fmt.Errorf("s3: put object %s: %w", key, err)
		}
		return size, timestamp, out.ETag, nil
	}

	// At or above the part size the uploader splits the seekable body into
	// section readers without copying it into part buffers.
	out, err := c.uploader.Upload(ctx, input)
	if err != nil {
		return 0, time.Time{}, nil, fmt.Errorf("s3: upload to %s: %w", key, err)
	}
	return size, timestamp, out.ETag, nil
}

// uploadStreamedLTX uploads from a reader of unknown size, buffering up to
// the part size to determine whether the object fits in a single PutObject.
func (c *ReplicaClient) uploadStreamedLTX(ctx context.Context, key string, r io.Reader, partSize int64) (int64, time.Time, *string, error) {
	// Use TeeReader to peek at LTX header while preserving data for upload
	var buf bytes.Buffer

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Unwrap the error (errors.As to smithy OperationError / awshttp.ResponseError) to see the HTTP status and S3 error code
  2. Verify credentials (aws sts get-caller-identity) and that s3:PutObject is allowed for the bucket/key prefix
  3. Confirm the bucket exists in the configured region and the region setting matches
  4. Check network path: DNS, proxy, VPC endpoint, and retry with backoff for transient 5xx/throttling
Defensive patterns

Strategy: retry

Validate before calling

// preflight before uploads
if _, err := c.s3.HeadBucket(ctx, &s3.HeadBucketInput{Bucket: bucket}); err != nil {
    return fmt.Errorf("bucket unreachable: %w", err)
}

Try / catch

var opErr *smithy.OperationError
if errors.As(err, &opErr) {
    var respErr *awshttp.ResponseError
    if errors.As(opErr.Err, &respErr) {
        // inspect respErr.HTTPStatusCode(): retry on 5xx/429, fix creds/perms on 4xx
    }
}

Prevention

When it happens

Trigger: PutObject returns an SDK error: connectivity loss, invalid AWS credentials, bucket does not exist or is in another region, IAM policy denies s3:PutObject, bucket policy/Object Lock rejects the write, or S3 throttling/slow-down responses.

Common situations: Rotated IAM keys not updated in the environment; region mismatch between client config and bucket; private-link/VPC endpoint misconfiguration; S3 outage or rate limiting during large replication bursts.

Understand the failure class

Background: "API request failed": what wrapped HTTP errors from external APIs mean and how to find the real cause — this error's family across 29 libraries.

Related errors


AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06). Data as JSON: /api/errors/77d938639ddf32c8. Report an issue: GitHub.