{"record":{"id":"c63b9288b6bdd93c","repo":"benbjohnson/litestream","slug":"s3-upload-failed-no-etag-returned","errorCode":null,"errorMessage":"s3: upload failed: no ETag returned","messagePattern":"s3: upload failed: no ETag returned","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"s3/replica_client.go","lineNumber":748,"sourceCode":"\n\t// The uploader rejects part sizes below the SDK minimum on every upload.\n\t// Fail identically here so the single-put path cannot mask the\n\t// misconfiguration for small objects.\n\tif partSize < manager.MinUploadPartSize {\n\t\treturn nil, fmt.Errorf(\"s3: upload to %s: part size must be at least %d bytes\", key, manager.MinUploadPartSize)\n\t}\n\n\tsize, timestamp, etag, err := c.uploadLTX(ctx, key, r, partSize)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tinternal.OperationTotalCounterVec.WithLabelValues(ReplicaClientType, \"PUT\").Inc()\n\tinternal.OperationBytesCounterVec.WithLabelValues(ReplicaClientType, \"PUT\").Add(float64(size))\n\n\t// ETag indicates successful upload\n\tif etag == nil {\n\t\treturn nil, fmt.Errorf(\"s3: upload failed: no ETag returned\")\n\t}\n\n\treturn &ltx.FileInfo{\n\t\tLevel:     level,\n\t\tMinTXID:   minTXID,\n\t\tMaxTXID:   maxTXID,\n\t\tSize:      size,\n\t\tCreatedAt: timestamp,\n\t}, nil\n}\n\n// uploadLTX uploads LTX data from r to key, choosing between a single\n// PutObject and the multipart uploader based on the object size.\nfunc (c *ReplicaClient) uploadLTX(ctx context.Context, key string, r io.Reader, partSize int64) (int64, time.Time, *string, error) {\n\t// The L0 replication path passes the local LTX file, so the size is\n\t// known up front and the body stays seekable for SDK retries.\n\tif rs, ok := r.(io.ReadSeeker); ok {\n\t\tif start, err := rs.Seek(0, io.SeekCurrent); err == nil {","sourceCodeStart":730,"sourceCodeEnd":766,"githubUrl":"https://github.com/benbjohnson/litestream/blob/4ed7a308f6271ebfd2b0a6e4b70b03011a37e4a3/s3/replica_client.go#L730-L766","documentation":"After a successful PutObject/upload the S3 response carried no ETag, which litestream treats as proof the upload did not actually persist. The SDK normally guarantees an ETag on success, so a nil ETag indicates an anomalous or non-standard S3-compatible endpoint. The upload is reported as failed so replication is retried rather than recorded as done.","triggerScenarios":"Calling WriteLTXFile against an S3-compatible storage provider (MinIO, Ceph, proprietary gateways, some NAS S3 gateways) whose PutObject response omits the ETag header.","commonSituations":"Using a third-party S3-compatible endpoint that does not fully implement the S3 API; a proxy or CDN stripping response headers; unusual multipart upload paths where the SDK fails to aggregate the ETag.","solutions":["Retry the upload; a transient gateway glitch usually returns a proper ETag on a second attempt","Verify the endpoint is fully S3-compatible (test a plain aws s3 cp and inspect returned ETag)","Upgrade the endpoint software (e.g. MinIO) to a version that returns ETag on PutObject","If the provider fundamentally never returns ETags, it is unsupported by litestream's S3 replica client; use a different replica backend"],"exampleFix":"null","handlingStrategy":"retry","validationCode":"null","typeGuard":null,"tryCatchPattern":"info, err := client.WriteLTXFile(ctx, key, r, level, minTXID, maxTXID, ts, partSize)\nif err != nil && strings.Contains(err.Error(), \"no ETag returned\") {\n    // retry once after backoff; if persistent, endpoint is not S3-compatible\n}","preventionTips":["Test S3-compatible endpoints return ETag headers before production use","Prefer well-known S3 implementations (AWS S3, MinIO current releases)","Monitor for repeated ETag errors per endpoint as a compatibility signal"],"tags":["s3","upload","etag","response"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"4ed7a308f6271ebfd2b0a6e4b70b03011a37e4a3","analyzedAt":"2026-09-06T18:29:25.564Z","contentChangedAt":"2026-09-06T18:29:25.564Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}