Repository: benbjohnson/litestream

Issue:
Error on aws ecs - aws-chunked encoding not supported
## Bug Description

Using litestream in docker container on ecs, syncing to s3. Using the latest version on arm (litestream-0.5.3-linux-arm64.tar.gz). 

Getting error

```time=2025-12-11T19:37:45.273Z` level=ERROR msg="monitor error" db=store.db replica=s3 error="write ltx file: s3: upload to database/0000/0000000000000002-0000000000000002.ltx: operation error S3: PutObject, https response error StatusCode: 400, RequestID: N9737YNY807MHFE6, HostID: NG1OyZ7gW3hml/tTrxS0zr0ZtisekLqRDinbP3w1FUj8gi27k48ZhojlRJK3L2izxLFKOaf6cfk=, api error InvalidArgument: aws-chunked encoding is not supported when x-amz-content-sha256 UNSIGNED-PAYLOAD is supplied."```

## Environment

**Litestream version:**
<!--- Run `litestream version` and paste output here -->
```text
0.5.3 arm64
```

**Operating system & version:**
AWS Linux 2 (using optimized ami for ecs from marketplace, official name [Amazon ECS-Optimized Amazon Linux 2023 (AL2023) arm64 AMI](https://console.aws.amazon.com/marketplace/search/listing/prodview-wc4bbpw5gappo?sr=0-8&ref_=beagle&applicationId=AWS-EC2-Console)
By[Amazon Web Services ](https://aws.amazon.com/marketplace/seller-profile?id=e6a5002c-6dd0-4d1e-8196-0a1d1857229b)
|
Ver 2023.0.20251209) 

**Installation method:**
Installed using docker file

**Storage backend:**
S3

## Steps to Reproduce
1. Step one 
Using this role (taken from documentation, minimal permissions needed for litestream)
```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetBucketLocation",
                "s3:ListBucket"
            ],
            "Resource": "arn:aws:s3:::bucket_name"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:DeleteObject",
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::bucket_name/database/*"
            ]
        }
    ]
}
```
2. Step two
Install in docker container 
```
ADD https://github.com/benbjohnson/litestream/releases/download/v0.5.3/litestream-v0.5.3-linux-arm64.tar.gz /tmp/litestream.tar.gz
RUN tar -C /usr/local/bin -xzf /tmp/litestream.tar.gz
```

3. Running in docker container with bash script (taken from example run.sh)

```
#!/bin/bash
set -e

# Restore the database from S3 if it exists
# -if-db-not-exists allows it to succeed if the DB is already there (e.g. on host mount)
# -if-replica-exists prevents failure on the very first run (empty bucket)
litestream restore -if-db-not-exists -if-replica-exists /some_dir/store.db

# Run the app wrapped in the replicate command
# Litestream will start, spawn your app as a subprocess, and sync changes to S3
exec litestream replicate -exec "uvicorn server.main:app --host 0.0.0.0 --port 8000"
```
**Expected behavior:**
I expect the app to work without errors. 

**Actual behavior:**
Getting above error

## Configuration

```yaml
dbs:
  - path: /some_dir/store.db
    replicas:
      - url: s3://bucket_name/database/
        region: my-region
```

</details>

## Logs
<!--- Include relevant error messages or logs -->
<!--- For more detail, run litestream with -trace flag -->
<!--- For long logs, consider using https://gist.github.com -->
<details><summary>Log output</summary>

```text
# Paste relevant logs here
```

</details>

## Additional Context

Downgrading to v0.3.13, it worked perfectly.


Discussion:
@duggan: I may have encountered a related problem that started with the update to 0.5.3:

```
litestream-1  | time=2025-12-11T21:33:15.356Z level=ERROR msg="monitor error" db=sqlite.db replica=s3 error="write ltx file: s3: upload to {REDACTED}/backups/0000/00000000000000f0-00000000000000f0.ltx: operation error S3: PutObject, https response error StatusCode: 403, RequestID: , HostID: , api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your secret access key and signing method."
```

Reverting to 0.5.2 resolved it for now.

I'm using Cloudflare R2.
Platform: x86_64, Debian bookworm.@nodesocket: @corylanou just upgraded to `0.5.3` and now seeing:

```
write ltx file: s3: upload to /0000/0000000000000001-0000000000000001.ltx: operation error S3: PutObject, https response error StatusCode: 400, RequestID: <redacted>, HostID: <redacted>, api error InvalidArgument: aws-chunked encoding is not supported when x-amz-content-sha256 UNSIGNED-PAYLOAD is supplied.
```

Using a S3 access point, via:

```
command: ["replicate", "/data/mydatabase.db", "s3://arn:aws:s3:us-east-2:<redacted>:accesspoint/my-s3-bucket-access-point-endpoint-here"]
```@araker: Wow this is fast, thanks for fixing it @corylanou. 

Solution patch:
diff --git a/s3/replica_client.go b/s3/replica_client.go
index e3e4867..0d0bb4e 100644
--- a/s3/replica_client.go
+++ b/s3/replica_client.go
@@ -552,6 +552,13 @@ func (c *ReplicaClient) middlewareOption() func(*middleware.Stack) error {
 			if err := v4.AddContentSHA256HeaderMiddleware(stack); err != nil {
 				return err
 			}
+
+			// Disable AWS SDK v2's trailing checksum middleware which uses
+			// aws-chunked encoding that is incompatible with UNSIGNED-PAYLOAD.
+			// AWS S3 rejects requests with: "aws-chunked encoding is not supported
+			// when x-amz-content-sha256 UNSIGNED-PAYLOAD is supplied."
+			// See: https://github.com/aws/aws-sdk-go-v2/discussions/2960
+			stack.Finalize.Remove("addInputChecksumTrailer")
 		}
 
 		if !c.RequireContentMD5 {



Test patch:
diff --git a/s3/replica_client_test.go b/s3/replica_client_test.go
index c9c8057..7239f72 100644
--- a/s3/replica_client_test.go
+++ b/s3/replica_client_test.go
@@ -153,6 +153,72 @@ func TestReplicaClientPayloadSigning(t *testing.T) {
 	}
 }
 
+func TestReplicaClient_UnsignedPayload_NoChunkedEncoding(t *testing.T) {
+	data := mustLTX(t)
+
+	headers := make(chan http.Header, 1)
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		defer r.Body.Close()
+		_, _ = io.Copy(io.Discard, r.Body)
+
+		if r.Method == http.MethodPut {
+			select {
+			case headers <- r.Header.Clone():
+			default:
+			}
+			w.Header().Set("ETag", `"test-etag"`)
+			w.WriteHeader(http.StatusOK)
+			return
+		}
+
+		w.WriteHeader(http.StatusOK)
+	}))
+	defer server.Close()
+
+	client := NewReplicaClient()
+	client.Bucket = "test-bucket"
+	client.Path = "replica"
+	client.Region = "us-east-1"
+	client.Endpoint = server.URL
+	client.ForcePathStyle = true
+	client.AccessKeyID = "test-access-key"
+	client.SecretAccessKey = "test-secret-key"
+	client.SignPayload = false
+
+	ctx := context.Background()
+	if err := client.Init(ctx); err != nil {
+		t.Fatalf("Init() error: %v", err)
+	}
+
+	if _, err := client.WriteLTXFile(ctx, 0, 2, 2, bytes.NewReader(data)); err != nil {
+		t.Fatalf("WriteLTXFile() error: %v", err)
+	}
+
+	select {
+	case hdr := <-headers:
+		if got := hdr.Get("x-amz-content-sha256"); got != "UNSIGNED-PAYLOAD" {
+			t.Errorf("x-amz-content-sha256 = %q, want UNSIGNED-PAYLOAD", got)
+		}
+
+		contentEnc := hdr.Get("Content-Encoding")
+		if strings.Contains(contentEnc, "aws-chunked") {
+			t.Errorf("Content-Encoding contains aws-chunked: %q; aws-chunked is incompatible with UNSIGNED-PAYLOAD", contentEnc)
+		}
+
+		transferEnc := hdr.Get("Transfer-Encoding")
+		if strings.Contains(transferEnc, "aws-chunked") {
+			t.Errorf("Transfer-Encoding contains aws-chunked: %q; aws-chunked is incompatible with UNSIGNED-PAYLOAD", transferEnc)
+		}
+
+		decoded := hdr.Get("X-Amz-Decoded-Content-Length")
+		if decoded != "" {
+			t.Errorf("X-Amz-Decoded-Content-Length = %q; this header indicates aws-chunked encoding which is incompatible with UNSIGNED-PAYLOAD", decoded)
+		}
+	case <-time.After(time.Second):
+		t.Fatal("timeout waiting for PUT request")
+	}
+}
+
 func mustLTX(t *testing.T) []byte {
 	t.Helper()