kopia/kopia · error · blob.ErrUnsupportedPutBlobOption

blob-retention

Error message

blob-retention

What it means

A static sentinel-style wrap indicating that the SFTP storage backend does not support put-blob retention options. sftpImpl.PutBlobInPath rejects PutOptions carrying retention settings because SFTP has no object-lock/retention mechanism.

Solutions

  1. Remove retention options from PutOptions when writing to SFTP storage.
  2. Route blobs that require retention to an S3 backend with object lock enabled instead.
  3. Check upstream caller/provider config so retention options are only set for supporting backends.
  4. If retention is required on SFTP, implement it externally (e.g. filesystem ACLs/cron cleanup) — the library cannot honor it.

Example fix

// before
err := st.PutBlob(ctx, blobID, data, blob.PutOptions{Retention: blob.RetentionOptions{...}})
// after
opts := blob.PutOptions{}
if s3Backend { opts.Retention = blob.RetentionOptions{...} }
err := st.PutBlob(ctx, blobID, data, opts)
Defensive patterns

Strategy: validation

Validate before calling

if opts.HasRetentionOptions() {
    return fmt.Errorf("retention options are not supported by the SFTP backend")
}
// call PutBlob only if validation passes

Type guard

null

Try / catch

if err := st.PutBlob(ctx, id, data, opts); err != nil {
    if strings.Contains(err.Error(), "blob-retention") { /* strip retention opts or use S3 backend */ }
}

Prevention

When it happens

Trigger: PutBlobInPath (or PutBlob) is called with blob.PutOptions where HasRetentionOptions() is true — e.g. callers setting retention mode/retain-until designed for S3 object lock.

Common situations: Code shared between S3 and SFTP backends passing the same PutOptions to both; enabling retention/immutable-backup features while using the SFTP provider; misconfigured storage profile requesting retention on a non-supporting backend.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


AI-assisted analysis of kopia/kopia@82495e54b5 (2026-09-07). Data as JSON: /api/errors/73562447233627b1. Report an issue: GitHub.

Appendix: source

Thrown at repo/blob/sftp/sftp_storage.go:196

		}

		if err != nil {
			return blob.Metadata{}, errors.Wrapf(err, "unrecognized error when calling stat() on SFTP file %v", fullPath)
		}

		return blob.Metadata{
			Length:    fi.Size(),
			Timestamp: fi.ModTime(),
		}, nil
	})
}

func (s *sftpImpl) PutBlobInPath(ctx context.Context, dirPath, fullPath string, data blob.Bytes, opts blob.PutOptions) error {
	_ = dirPath

	switch {
	case opts.HasRetentionOptions():
		return errors.Wrap(blob.ErrUnsupportedPutBlobOption, "blob-retention")
	case opts.DoNotRecreate:
		return errors.Wrap(blob.ErrUnsupportedPutBlobOption, "do-not-recreate")
	}

	// SFTP client Write() does not do any buffering leading to sub-optimal
	// performance of gather writes, so we copy the data to a contiguous
	// temporary buffer first.
	contig := gather.NewWriteBufferMaxContiguous()
	defer contig.Close()

	if _, err := data.WriteTo(contig); err != nil {
		return errors.Wrap(err, "can't write to contiguous buffer")
	}

	//nolint:wrapcheck
	return s.rec.UsingConnectionNoResult(ctx, "PutBlobInPath", func(conn connection.Connection) error {
		randSuffix := make([]byte, tempFileRandomSuffixLen)
		if _, err := rand.Read(randSuffix); err != nil {

View on GitHub (pinned to 82495e54b5)